diff --git a/harbour-fernschreiber.pro b/harbour-fernschreiber.pro
index 3915558..2961f7c 100644
--- a/harbour-fernschreiber.pro
+++ b/harbour-fernschreiber.pro
@@ -14,10 +14,12 @@ TARGET = harbour-fernschreiber
CONFIG += sailfishapp sailfishapp_i18n
-PKGCONFIG += nemonotifications-qt5 ngf-qt5
+PKGCONFIG += nemonotifications-qt5 ngf-qt5 zlib
QT += core dbus sql
+DEFINES += QT_STATICPLUGIN
+
SOURCES += src/harbour-fernschreiber.cpp \
src/appsettings.cpp \
src/chatlistmodel.cpp \
@@ -31,7 +33,8 @@ SOURCES += src/harbour-fernschreiber.cpp \
src/stickermanager.cpp \
src/tdlibfile.cpp \
src/tdlibreceiver.cpp \
- src/tdlibwrapper.cpp
+ src/tdlibwrapper.cpp \
+ src/tgsplugin.cpp
DISTFILES += qml/harbour-fernschreiber.qml \
qml/components/AudioPreview.qml \
@@ -145,7 +148,8 @@ HEADERS += \
src/tdlibfile.h \
src/tdlibreceiver.h \
src/tdlibsecrets.h \
- src/tdlibwrapper.h
+ src/tdlibwrapper.h \
+ src/tgsplugin.h
# https://github.com/Samsung/rlottie.git
diff --git a/qml/components/StickerPreview.qml b/qml/components/StickerPreview.qml
index 89d6ba0..8370006 100644
--- a/qml/components/StickerPreview.qml
+++ b/qml/components/StickerPreview.qml
@@ -18,76 +18,77 @@
*/
import QtQuick 2.6
import Sailfish.Silica 1.0
+import WerkWolf.Fernschreiber 1.0
Item {
-
property ListItem messageListItem
- property var rawMessage: messageListItem.myMessage
- property var stickerData: rawMessage.content.sticker;
- property int usedFileId;
+ readonly property var stickerData: messageListItem.myMessage.content.sticker;
+ readonly property bool animated: stickerData.is_animated
+ readonly property bool stickerVisible: staticStickerLoader.item ? staticStickerLoader.item.visible :
+ animatedStickerLoader.item ? animatedStickerLoader.item.visible : false
- width: stickerData.width
- height: stickerData.height
+ implicitWidth: stickerData.width
+ implicitHeight: stickerData.height
- Component.onCompleted: {
- if (stickerData) {
- if (stickerData.is_animated) {
- // Use thumbnail until we can decode TGS files
- usedFileId = stickerData.thumbnail.photo.id;
- if (stickerData.thumbnail.photo.local.is_downloading_completed) {
- stickerImage.source = stickerData.thumbnail.photo.local.path;
- } else {
- tdLibWrapper.downloadFile(usedFileId);
- }
- } else {
- usedFileId = stickerData.sticker.id;
- if (stickerData.sticker.local.is_downloading_completed) {
- stickerImage.source = stickerData.sticker.local.path;
- } else {
- tdLibWrapper.downloadFile(usedFileId);
+ TDLibFile {
+ id: file
+ tdlib: tdLibWrapper
+ fileInformation: stickerData.sticker
+ autoLoad: true
+ }
+
+ Item {
+ width: stickerData.width
+ height: stickerData.height
+ // (centered in image mode, text-like in sticker mode)
+ x: appSettings.showStickersAsImages ? (parent.width - width)/2 :
+ messageListItem.isOwnMessage ? (parent.width - width) : 0
+ anchors.verticalCenter: parent.verticalCenter
+
+ 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 {
- target: tdLibWrapper
- onFileUpdated: {
- if (stickerData) {
- if (fileId === usedFileId && fileInformation.local.is_downloading_completed) {
- if (stickerData.is_animated) {
- stickerData.thumbnail.photo = fileInformation;
- } else {
- stickerData.sticker = fileInformation;
- }
- stickerImage.source = fileInformation.local.path;
+ Loader {
+ id: staticStickerLoader
+ anchors.fill: parent
+ active: !animated
+ sourceComponent: Component {
+ Image {
+ anchors.fill: parent
+ source: file.path
+ fillMode: Image.PreserveAspectFit
+ autoTransform: true
+ asynchronous: true
+ visible: opacity > 0
+ opacity: status === Image.Ready ? 1 : 0
+ Behavior on opacity { FadeAnimation {} }
}
}
}
- }
- Image {
- id: stickerImage
- anchors.fill: parent
+ Loader {
+ anchors.fill: parent
+ sourceComponent: Component {
+ BackgroundImage {}
+ }
- fillMode: Image.PreserveAspectFit
- autoTransform: true
- asynchronous: true
- visible: opacity > 0
- opacity: status === Image.Ready ? 1 : 0
- Behavior on opacity { FadeAnimation {} }
- }
-
- Loader {
- anchors.fill: parent
- sourceComponent: Component {
- BackgroundImage {}
+ active: opacity > 0
+ opacity: !stickerVisible && !placeHolderDelayTimer.running ? 0.15 : 0
+ Behavior on opacity { FadeAnimation {} }
}
-
- active: opacity > 0
- opacity: !stickerImage.visible && !placeHolderDelayTimer.running ? 0.15 : 0
- Behavior on opacity { FadeAnimation {} }
}
Timer {
diff --git a/src/harbour-fernschreiber.cpp b/src/harbour-fernschreiber.cpp
index 614ec47..5491f65 100644
--- a/src/harbour-fernschreiber.cpp
+++ b/src/harbour-fernschreiber.cpp
@@ -38,6 +38,9 @@
#include "dbusadaptor.h"
#include "processlauncher.h"
#include "stickermanager.h"
+#include "tgsplugin.h"
+
+Q_IMPORT_PLUGIN(TgsIOPlugin)
int main(int argc, char *argv[])
{
diff --git a/src/tgsplugin.cpp b/src/tgsplugin.cpp
new file mode 100644
index 0000000..3bd67ed
--- /dev/null
+++ b/src/tgsplugin.cpp
@@ -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 .
+*/
+
+#include "tgsplugin.h"
+#include "rlottie.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+#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 currentRender;
+ std::unique_ptr 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(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);
+}
diff --git a/src/tgsplugin.h b/src/tgsplugin.h
new file mode 100644
index 0000000..dfadcf4
--- /dev/null
+++ b/src/tgsplugin.h
@@ -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 .
+*/
+
+#ifndef TGS_IMAGE_IO_PLUGIN
+#define TGS_IMAGE_IO_PLUGIN
+
+#include
+#include
+
+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
diff --git a/src/tgsplugin.json b/src/tgsplugin.json
new file mode 100644
index 0000000..ac65e14
--- /dev/null
+++ b/src/tgsplugin.json
@@ -0,0 +1,3 @@
+{
+ "Keys": [ "tgs" ]
+}