Merge pull request #140 from monich/lottie

Support for animated stickers
This commit is contained in:
Sebastian Wolf 2020-11-11 12:29:51 +01:00 committed by GitHub
commit 2b139b926d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 508 additions and 57 deletions

3
.gitmodules vendored Normal file
View file

@ -0,0 +1,3 @@
[submodule "rlottie"]
path = rlottie
url = https://github.com/Samsung/rlottie.git

View file

@ -14,10 +14,12 @@ TARGET = harbour-fernschreiber
CONFIG += sailfishapp sailfishapp_i18n CONFIG += sailfishapp sailfishapp_i18n
PKGCONFIG += nemonotifications-qt5 ngf-qt5 PKGCONFIG += nemonotifications-qt5 ngf-qt5 zlib
QT += core dbus sql QT += core dbus sql
DEFINES += QT_STATICPLUGIN
SOURCES += src/harbour-fernschreiber.cpp \ SOURCES += src/harbour-fernschreiber.cpp \
src/appsettings.cpp \ src/appsettings.cpp \
src/chatlistmodel.cpp \ src/chatlistmodel.cpp \
@ -31,7 +33,8 @@ SOURCES += src/harbour-fernschreiber.cpp \
src/stickermanager.cpp \ src/stickermanager.cpp \
src/tdlibfile.cpp \ src/tdlibfile.cpp \
src/tdlibreceiver.cpp \ src/tdlibreceiver.cpp \
src/tdlibwrapper.cpp src/tdlibwrapper.cpp \
src/tgsplugin.cpp
DISTFILES += qml/harbour-fernschreiber.qml \ DISTFILES += qml/harbour-fernschreiber.qml \
qml/components/AudioPreview.qml \ qml/components/AudioPreview.qml \
@ -145,4 +148,69 @@ HEADERS += \
src/tdlibfile.h \ src/tdlibfile.h \
src/tdlibreceiver.h \ src/tdlibreceiver.h \
src/tdlibsecrets.h \ src/tdlibsecrets.h \
src/tdlibwrapper.h src/tdlibwrapper.h \
src/tgsplugin.h
# https://github.com/Samsung/rlottie.git
RLOTTIE_CONFIG = $${PWD}/rlottie/src/vector/config.h
PRE_TARGETDEPS += $${RLOTTIE_CONFIG}
QMAKE_EXTRA_TARGETS += rlottie_config
rlottie_config.target = $${RLOTTIE_CONFIG}
rlottie_config.commands = touch $${RLOTTIE_CONFIG} # Empty config is fine
DEFINES += LOTTIE_THREAD_SUPPORT
INCLUDEPATH += \
rlottie/inc \
rlottie/src/vector \
rlottie/src/vector/freetype
SOURCES += \
rlottie/src/lottie/lottieanimation.cpp \
rlottie/src/lottie/lottieitem.cpp \
rlottie/src/lottie/lottieitem_capi.cpp \
rlottie/src/lottie/lottiekeypath.cpp \
rlottie/src/lottie/lottieloader.cpp \
rlottie/src/lottie/lottiemodel.cpp \
rlottie/src/lottie/lottieparser.cpp
SOURCES += \
rlottie/src/vector/freetype/v_ft_math.cpp \
rlottie/src/vector/freetype/v_ft_raster.cpp \
rlottie/src/vector/freetype/v_ft_stroker.cpp \
rlottie/src/vector/stb/stb_image.cpp \
rlottie/src/vector/varenaalloc.cpp \
rlottie/src/vector/vbezier.cpp \
rlottie/src/vector/vbitmap.cpp \
rlottie/src/vector/vbrush.cpp \
rlottie/src/vector/vdasher.cpp \
rlottie/src/vector/vdrawable.cpp \
rlottie/src/vector/vdrawhelper.cpp \
rlottie/src/vector/vdrawhelper_common.cpp \
rlottie/src/vector/vdrawhelper_neon.cpp \
rlottie/src/vector/vdrawhelper_sse2.cpp \
rlottie/src/vector/vmatrix.cpp \
rlottie/src/vector/vimageloader.cpp \
rlottie/src/vector/vinterpolator.cpp \
rlottie/src/vector/vpainter.cpp \
rlottie/src/vector/vpath.cpp \
rlottie/src/vector/vpathmesure.cpp \
rlottie/src/vector/vraster.cpp \
rlottie/src/vector/vrle.cpp
NEON = $$system(g++ -dM -E -x c++ - < /dev/null | grep __ARM_NEON__)
SSE2 = $$system(g++ -dM -E -x c++ - < /dev/null | grep __SSE2__)
!isEmpty(NEON) {
message(Using NEON render functions)
SOURCES += rlottie/src/vector/pixman/pixman-arm-neon-asm.S
} else {
!isEmpty(SSE2) {
message(Using SSE2 render functions)
SOURCES += rlottie/src/vector/vdrawhelper_sse2.cpp
} else {
message(Using default render functions)
}
}

View file

@ -18,76 +18,77 @@
*/ */
import QtQuick 2.6 import QtQuick 2.6
import Sailfish.Silica 1.0 import Sailfish.Silica 1.0
import WerkWolf.Fernschreiber 1.0
Item { Item {
property ListItem messageListItem property ListItem messageListItem
property var rawMessage: messageListItem.myMessage
property var stickerData: rawMessage.content.sticker; readonly property var stickerData: messageListItem.myMessage.content.sticker;
property int usedFileId; readonly property bool animated: stickerData.is_animated && appSettings.animateStickers
readonly property bool stickerVisible: staticStickerLoader.item ? staticStickerLoader.item.visible :
animatedStickerLoader.item ? animatedStickerLoader.item.visible : false
width: stickerData.width implicitWidth: stickerData.width
height: stickerData.height implicitHeight: stickerData.height
Component.onCompleted: { TDLibFile {
if (stickerData) { id: file
if (stickerData.is_animated) { tdlib: tdLibWrapper
// Use thumbnail until we can decode TGS files fileInformation: stickerData.sticker
usedFileId = stickerData.thumbnail.photo.id; autoLoad: true
if (stickerData.thumbnail.photo.local.is_downloading_completed) { }
stickerImage.source = stickerData.thumbnail.photo.local.path;
} else { Item {
tdLibWrapper.downloadFile(usedFileId); width: stickerData.width
} height: stickerData.height
} else { // (centered in image mode, text-like in sticker mode)
usedFileId = stickerData.sticker.id; x: appSettings.showStickersAsImages ? (parent.width - width)/2 :
if (stickerData.sticker.local.is_downloading_completed) { messageListItem.isOwnMessage ? (parent.width - width) : 0
stickerImage.source = stickerData.sticker.local.path; anchors.verticalCenter: parent.verticalCenter
} else {
tdLibWrapper.downloadFile(usedFileId); Loader {
id: animatedStickerLoader
anchors.fill: parent
active: animated
sourceComponent: Component {
AnimatedImage {
anchors.fill: parent
source: file.path
asynchronous: true
paused: !Qt.application.active
cache: false
} }
} }
} }
}
Connections { Loader {
target: tdLibWrapper id: staticStickerLoader
onFileUpdated: { anchors.fill: parent
if (stickerData) { active: !animated
if (fileId === usedFileId && fileInformation.local.is_downloading_completed) { sourceComponent: Component {
if (stickerData.is_animated) { Image {
stickerData.thumbnail.photo = fileInformation; anchors.fill: parent
} else { source: file.path
stickerData.sticker = fileInformation; fillMode: Image.PreserveAspectFit
} autoTransform: true
stickerImage.source = fileInformation.local.path; asynchronous: true
visible: opacity > 0
opacity: status === Image.Ready ? 1 : 0
Behavior on opacity { FadeAnimation {} }
} }
} }
} }
}
Image { Loader {
id: stickerImage anchors.fill: parent
anchors.fill: parent sourceComponent: Component {
BackgroundImage {}
}
fillMode: Image.PreserveAspectFit active: opacity > 0
autoTransform: true opacity: !stickerVisible && !placeHolderDelayTimer.running ? 0.15 : 0
asynchronous: true Behavior on opacity { FadeAnimation {} }
visible: opacity > 0
opacity: status === Image.Ready ? 1 : 0
Behavior on opacity { FadeAnimation {} }
}
Loader {
anchors.fill: parent
sourceComponent: Component {
BackgroundImage {}
} }
active: opacity > 0
opacity: !stickerImage.visible && !placeHolderDelayTimer.running ? 0.15 : 0
Behavior on opacity { FadeAnimation {} }
} }
Timer { Timer {

View file

@ -117,6 +117,15 @@ Page {
text: qsTr("Appearance") text: qsTr("Appearance")
} }
TextSwitch {
checked: appSettings.animateStickers
text: qsTr("Animate stickers")
automaticCheck: false
onClicked: {
appSettings.animateStickers = !checked
}
}
TextSwitch { TextSwitch {
checked: appSettings.showStickersAsImages checked: appSettings.showStickersAsImages
text: qsTr("Show stickers as images") text: qsTr("Show stickers as images")

1
rlottie Submodule

@ -0,0 +1 @@
Subproject commit bf3d272df3916a0c34575ac8286cb0fe672fd0d4

View file

@ -24,6 +24,7 @@ namespace {
const QString KEY_SEND_BY_ENTER("sendByEnter"); const QString KEY_SEND_BY_ENTER("sendByEnter");
const QString KEY_USE_OPEN_WITH("useOpenWith"); const QString KEY_USE_OPEN_WITH("useOpenWith");
const QString KEY_SHOW_STICKERS_AS_IMAGES("showStickersAsImages"); const QString KEY_SHOW_STICKERS_AS_IMAGES("showStickersAsImages");
const QString KEY_ANIMATE_STICKERS("animateStickers");
const QString KEY_NOTIFICATION_FEEDBACK("notificationFeedback"); const QString KEY_NOTIFICATION_FEEDBACK("notificationFeedback");
} }
@ -73,6 +74,20 @@ void AppSettings::setShowStickersAsImages(bool showAsImages)
} }
} }
bool AppSettings::animateStickers() const
{
return settings.value(KEY_ANIMATE_STICKERS, true).toBool();
}
void AppSettings::setAnimateStickers(bool animate)
{
if (animateStickers() != animate) {
LOG(KEY_ANIMATE_STICKERS << animate);
settings.setValue(KEY_ANIMATE_STICKERS, animate);
emit animateStickersChanged();
}
}
AppSettings::NotificationFeedback AppSettings::notificationFeedback() const AppSettings::NotificationFeedback AppSettings::notificationFeedback() const
{ {
return (NotificationFeedback) settings.value(KEY_NOTIFICATION_FEEDBACK, (int) NotificationFeedbackAll).toInt(); return (NotificationFeedback) settings.value(KEY_NOTIFICATION_FEEDBACK, (int) NotificationFeedbackAll).toInt();

View file

@ -26,6 +26,7 @@ class AppSettings : public QObject {
Q_PROPERTY(bool sendByEnter READ getSendByEnter WRITE setSendByEnter NOTIFY sendByEnterChanged) Q_PROPERTY(bool sendByEnter READ getSendByEnter WRITE setSendByEnter NOTIFY sendByEnterChanged)
Q_PROPERTY(bool useOpenWith READ getUseOpenWith WRITE setUseOpenWith NOTIFY useOpenWithChanged) Q_PROPERTY(bool useOpenWith READ getUseOpenWith WRITE setUseOpenWith NOTIFY useOpenWithChanged)
Q_PROPERTY(bool showStickersAsImages READ showStickersAsImages WRITE setShowStickersAsImages NOTIFY showStickersAsImagesChanged) Q_PROPERTY(bool showStickersAsImages READ showStickersAsImages WRITE setShowStickersAsImages NOTIFY showStickersAsImagesChanged)
Q_PROPERTY(bool animateStickers READ animateStickers WRITE setAnimateStickers NOTIFY animateStickersChanged)
Q_PROPERTY(NotificationFeedback notificationFeedback READ notificationFeedback WRITE setNotificationFeedback NOTIFY notificationFeedbackChanged) Q_PROPERTY(NotificationFeedback notificationFeedback READ notificationFeedback WRITE setNotificationFeedback NOTIFY notificationFeedbackChanged)
public: public:
@ -48,6 +49,9 @@ public:
bool showStickersAsImages() const; bool showStickersAsImages() const;
void setShowStickersAsImages(bool showAsImages); void setShowStickersAsImages(bool showAsImages);
bool animateStickers() const;
void setAnimateStickers(bool animate);
NotificationFeedback notificationFeedback() const; NotificationFeedback notificationFeedback() const;
void setNotificationFeedback(NotificationFeedback feedback); void setNotificationFeedback(NotificationFeedback feedback);
@ -55,6 +59,7 @@ signals:
void sendByEnterChanged(); void sendByEnterChanged();
void useOpenWithChanged(); void useOpenWithChanged();
void showStickersAsImagesChanged(); void showStickersAsImagesChanged();
void animateStickersChanged();
void notificationFeedbackChanged(); void notificationFeedbackChanged();
private: private:

View file

@ -38,6 +38,9 @@
#include "dbusadaptor.h" #include "dbusadaptor.h"
#include "processlauncher.h" #include "processlauncher.h"
#include "stickermanager.h" #include "stickermanager.h"
#include "tgsplugin.h"
Q_IMPORT_PLUGIN(TgsIOPlugin)
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {

310
src/tgsplugin.cpp Normal file
View file

@ -0,0 +1,310 @@
/*
Copyright (C) 2020 Slava Monich at al.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "tgsplugin.h"
#include "rlottie.h"
#include <QDebug>
#include <QSize>
#include <QImage>
#include <QImageIOHandler>
#include <QFileDevice>
#include <QFileInfo>
#include <zlib.h>
#define LOG(x) qDebug() << "[TgsIOHandler]" << qPrintable(fileName) << x
#define WARN(x) qWarning() << "[TgsIOHandler]" << qPrintable(fileName) << x
class TgsIOHandler : public QImageIOHandler
{
public:
static const QByteArray NAME;
static const QByteArray GZ_MAGIC;
typedef std::string ByteArray;
TgsIOHandler(QIODevice* device, const QByteArray& format);
~TgsIOHandler();
ByteArray uncompress();
bool load();
void render(int frameIndex);
void finishRendering();
// QImageIOHandler
bool canRead() const Q_DECL_OVERRIDE;
QByteArray name() const Q_DECL_OVERRIDE;
bool read(QImage* image) Q_DECL_OVERRIDE;
QVariant option(ImageOption option) const Q_DECL_OVERRIDE;
bool supportsOption(ImageOption option) const Q_DECL_OVERRIDE;
bool jumpToNextImage() Q_DECL_OVERRIDE;
bool jumpToImage(int imageNumber) Q_DECL_OVERRIDE;
int loopCount() const Q_DECL_OVERRIDE;
int imageCount() const Q_DECL_OVERRIDE;
int nextImageDelay() const Q_DECL_OVERRIDE;
int currentImageNumber() const Q_DECL_OVERRIDE;
QRect currentImageRect() const Q_DECL_OVERRIDE;
public:
QString fileName;
QSize size;
qreal frameRate;
int frameCount;
int currentFrame;
QImage firstImage;
QImage prevImage;
QImage currentImage;
std::future<rlottie::Surface> currentRender;
std::unique_ptr<rlottie::Animation> animation;
};
const QByteArray TgsIOHandler::NAME("tgs");
const QByteArray TgsIOHandler::GZ_MAGIC("\x1f\x8b");
TgsIOHandler::TgsIOHandler(QIODevice* device, const QByteArray& format) :
frameRate(0.),
frameCount(0),
currentFrame(0)
{
QFileDevice* file = qobject_cast<QFileDevice*>(device);
if (file) {
fileName = QFileInfo(file->fileName()).fileName();
}
setDevice(device);
setFormat(format);
}
TgsIOHandler::~TgsIOHandler()
{
if (currentRender.valid()) {
currentRender.get();
}
LOG("Done");
}
TgsIOHandler::ByteArray TgsIOHandler::uncompress()
{
ByteArray unzipped;
const QByteArray zipped(device()->readAll());
if (!zipped.isEmpty()) {
z_stream unzip;
memset(&unzip, 0, sizeof(unzip));
unzip.next_in = (Bytef*)zipped.constData();
// Add 16 for decoding gzip header
int zerr = inflateInit2(&unzip, MAX_WBITS + 16);
if (zerr == Z_OK) {
const uint chunk = 0x1000;
unzipped.resize(chunk);
unzip.next_out = (Bytef*)unzipped.data();
unzip.avail_in = zipped.size();
unzip.avail_out = chunk;
LOG("Compressed size" << zipped.size());
while (unzip.avail_out > 0 && zerr == Z_OK) {
zerr = inflate(&unzip, Z_NO_FLUSH);
if (zerr == Z_OK && unzip.avail_out < chunk) {
// Data may get relocated, update next_out too
unzipped.resize(unzipped.size() + chunk);
unzip.next_out = (Bytef*)unzipped.data() + unzip.total_out;
unzip.avail_out += chunk;
}
}
if (zerr == Z_STREAM_END) {
unzipped.resize(unzip.next_out - (Bytef*)unzipped.data());
LOG("Uncompressed size" << unzipped.size());
} else {
unzipped.clear();
}
inflateEnd(&unzip);
}
}
return unzipped;
}
bool TgsIOHandler::load()
{
if (!animation && device()) {
ByteArray json(uncompress());
if (json.size() > 0) {
animation = rlottie::Animation::loadFromData(json, std::string(), std::string(), false);
if (animation) {
size_t width, height;
animation->size(width, height);
frameRate = animation->frameRate();
frameCount = (int) animation->totalFrame();
size = QSize(width, height);
LOG(size << frameCount << "frames," << frameRate << "fps");
render(0); // Pre-render first frame
}
}
}
return animation != Q_NULLPTR;
}
void TgsIOHandler::finishRendering()
{
if (currentRender.valid()) {
currentRender.get();
prevImage = currentImage;
if (!currentFrame && !firstImage.isNull()) {
LOG("Rendered first frame");
firstImage = currentImage;
}
} else {
// Must be the first frame
prevImage = currentImage;
}
}
void TgsIOHandler::render(int frameIndex)
{
currentFrame = frameIndex % frameCount;
if (!currentFrame && !firstImage.isNull()) {
// The first frame only gets rendered once
currentImage = firstImage;
} else {
const int width = (int)size.width();
const int height = (int)size.height();
currentImage = QImage(width, height, QImage::Format_ARGB32_Premultiplied);
currentRender = animation->render(currentFrame,
rlottie::Surface((uint32_t*)currentImage.bits(),
width, height, currentImage.bytesPerLine()));
}
}
bool TgsIOHandler::read(QImage* out)
{
if (load() && frameCount > 0) {
// We must have the first frame, will wait if necessary
if (currentFrame && currentRender.valid()) {
std::future_status status = currentRender.wait_for(std::chrono::milliseconds(0));
if (status != std::future_status::ready) {
LOG("Skipping frame" << currentFrame);
currentFrame = (currentFrame + 1) % frameCount;
*out = prevImage;
return true;
}
}
finishRendering();
*out = currentImage;
render(currentFrame + 1);
return true;
}
return false;
}
bool TgsIOHandler::canRead() const
{
if (!device()) {
return false;
} else if (animation) {
return true;
} else {
// Need to support uncompressed data?
return device()->peek(2) == GZ_MAGIC;
}
}
QByteArray TgsIOHandler::name() const
{
return NAME;
}
QVariant TgsIOHandler::option(ImageOption option) const
{
switch (option) {
case Size:
((TgsIOHandler*)this)->load(); // Cast off const
return size;
case Animation:
return true;
case ImageFormat:
return QImage::Format_ARGB32_Premultiplied;
default:
break;
}
return QVariant();
}
bool TgsIOHandler::supportsOption(ImageOption option) const
{
switch(option) {
case Size:
case Animation:
case ImageFormat:
return true;
default:
break;
}
return false;
}
bool TgsIOHandler::jumpToNextImage()
{
if (frameCount) {
finishRendering();
render(currentFrame + 1);
return true;
}
return false;
}
bool TgsIOHandler::jumpToImage(int imageNumber)
{
if (frameCount) {
if (imageNumber != currentFrame) {
finishRendering();
render(imageNumber);
}
return true;
}
return false;
}
int TgsIOHandler::loopCount() const
{
return -1;
}
int TgsIOHandler::imageCount() const
{
return frameCount;
}
int TgsIOHandler::currentImageNumber() const
{
return currentFrame;
}
QRect TgsIOHandler::currentImageRect() const
{
return QRect(QPoint(), size);
}
int TgsIOHandler::nextImageDelay() const
{
return frameRate > 0 ? (int)(1000/frameRate) : 33;
}
QImageIOPlugin::Capabilities TgsIOPlugin::capabilities(QIODevice*, const QByteArray& format) const
{
return Capabilities((format == TgsIOHandler::NAME) ? CanRead : 0);
}
QImageIOHandler* TgsIOPlugin::create(QIODevice* device, const QByteArray& format) const
{
return new TgsIOHandler(device, format);
}

33
src/tgsplugin.h Normal file
View file

@ -0,0 +1,33 @@
/*
Copyright (C) 2020 Slava Monich at al.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef TGS_IMAGE_IO_PLUGIN
#define TGS_IMAGE_IO_PLUGIN
#include <QStringList>
#include <QImageIOPlugin>
class TgsIOPlugin : public QImageIOPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QImageIOHandlerFactoryInterface" FILE "tgsplugin.json")
public:
Capabilities capabilities(QIODevice* device, const QByteArray& format) const Q_DECL_OVERRIDE;
QImageIOHandler* create(QIODevice* device, const QByteArray& format) const Q_DECL_OVERRIDE;
};
#endif // TGS_IMAGE_IO_PLUGIN

3
src/tgsplugin.json Normal file
View file

@ -0,0 +1,3 @@
{
"Keys": [ "tgs" ]
}