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" ] +}