diff --git a/LICENSES/MIT.txt b/LICENSES/MIT.txt new file mode 100755 index 000000000..204b93da4 --- /dev/null +++ b/LICENSES/MIT.txt @@ -0,0 +1,19 @@ +MIT License Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice (including the next +paragraph) shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF +OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/imports/NeoChat/Component/FullScreenImage.qml b/imports/NeoChat/Component/FullScreenImage.qml index 4bec39711..3773e65ae 100644 --- a/imports/NeoChat/Component/FullScreenImage.qml +++ b/imports/NeoChat/Component/FullScreenImage.qml @@ -11,6 +11,9 @@ ApplicationWindow { property string filename property url localPath + property string blurhash: "" + property int imageWidth: -1 + property int imageHeight: -1 flags: Qt.FramelessWindowHint | Qt.WA_TranslucentBackground visibility: Qt.WindowFullScreen @@ -29,22 +32,29 @@ ApplicationWindow { } BusyIndicator { - visible: image.status !== Image.Ready - anchors.centerIn: parent - running: visible + visible: image.status !== Image.Ready && root.blurhash === "" + anchors.centerIn: parent + running: visible } AnimatedImage { - id: image + id: image anchors.centerIn: parent - width: Math.min(sourceSize.width, root.width) - height: Math.min(sourceSize.height, root.height) + width: Math.min(root.imageWidth !== -1 ? root.imageWidth : sourceSize.width, root.width) + height: Math.min(root.imageHeight !== -1 ? root.imageWidth : sourceSize.height, root.height) - cache: false fillMode: Image.PreserveAspectFit source: localPath + + Image { + anchors.centerIn: parent + width: image.width + height: image.height + source: root.blurhash !== "" ? ("image://blurhash/" + root.blurhash) : "" + visible: root.blurhash !== "" && parent.status !== Image.Ready + } } Button { diff --git a/imports/NeoChat/Component/Timeline/ImageDelegate.qml b/imports/NeoChat/Component/Timeline/ImageDelegate.qml index 075278d5f..fc41d77bc 100644 --- a/imports/NeoChat/Component/Timeline/ImageDelegate.qml +++ b/imports/NeoChat/Component/Timeline/ImageDelegate.qml @@ -29,6 +29,12 @@ Image { source: "image://mxc/" + mediaId + Image { + anchors.fill: parent + source: "image://blurhash/" + content.info["xyz.amorgan.blurhash"] + visible: parent.status !== Image.Ready + } + fillMode: Image.PreserveAspectFit ToolTip.text: display diff --git a/imports/NeoChat/Page/RoomPage.qml b/imports/NeoChat/Page/RoomPage.qml index e0e7a058e..9a4a777a7 100644 --- a/imports/NeoChat/Page/RoomPage.qml +++ b/imports/NeoChat/Page/RoomPage.qml @@ -457,7 +457,7 @@ Kirigami.ScrollablePage { acceptedButtons: Qt.LeftButton onLongPressed: openFileContext(author, model.display, eventId, toolTip, progressInfo, parent) onTapped: { - fullScreenImage.createObject(parent, {"filename": eventId, "localPath": currentRoom.urlToDownload(eventId)}).showFullScreen() + fullScreenImage.createObject(parent, {"filename": eventId, "localPath": currentRoom.urlToDownload(eventId), "blurhash": model.content.info["xyz.amorgan.blurhash"], "imageWidth": content.info.w, "imageHeight": content.info.h}).showFullScreen() } } } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ad334224e..96b8c060e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -34,6 +34,8 @@ add_executable(neochat commandmodel.cpp webshortcutmodel.cpp spellcheckhighlighter.cpp + blurhash.cpp + blurhashimageprovider.cpp ../res.qrc ) diff --git a/src/blurhash.cpp b/src/blurhash.cpp new file mode 100644 index 000000000..9681d78db --- /dev/null +++ b/src/blurhash.cpp @@ -0,0 +1,186 @@ +// SPDX-FileCopyrightText: 2018 Wolt Enterprises +// SPDX-License-Identifiert: MIT + +#include "blurhash.h" + +static char chars[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~"; + +static inline int linearTosRGB(float value) +{ + float v = fmaxf(0, fminf(1, value)); + if (v <= 0.0031308) + return v * 12.92 * 255 + 0.5; + else + return (1.055 * powf(v, 1 / 2.4) - 0.055) * 255 + 0.5; +} + +static inline float sRGBToLinear(int value) +{ + float v = (float)value / 255; + if (v <= 0.04045) + return v / 12.92; + else + return powf((v + 0.055) / 1.055, 2.4); +} + +static inline float signPow(float value, float exp) +{ + return copysignf(powf(fabsf(value), exp), value); +} + +static inline uint8_t clampToUByte(int *src) +{ + if (*src >= 0 && *src <= 255) { + return *src; + } + return (*src < 0) ? 0 : 255; +} + +static inline uint8_t *createByteArray(int size) +{ + return (uint8_t *)malloc(size * sizeof(uint8_t)); +} + +int decodeToInt(const char *string, int start, int end) +{ + int value = 0; + for (int iter1 = start; iter1 < end; iter1++) { + int index = -1; + for (int iter2 = 0; iter2 < 83; iter2++) { + if (chars[iter2] == string[iter1]) { + index = iter2; + break; + } + } + if (index == -1) { + return -1; + } + value = value * 83 + index; + } + return value; +} + +bool isValidBlurhash(const char *blurhash) +{ + const int hashLength = strlen(blurhash); + + if (!blurhash || strlen(blurhash) < 6) { + return false; + } + + int sizeFlag = decodeToInt(blurhash, 0, 1); + int numY = (int)floorf(sizeFlag / 9) + 1; + int numX = (sizeFlag % 9) + 1; + + return hashLength == 4 + 2 * numX * numY; +} + +void decodeDC(int value, float *r, float *g, float *b) +{ + *r = sRGBToLinear(value >> 16); + *g = sRGBToLinear((value >> 8) & 255); + *b = sRGBToLinear(value & 255); +} + +void decodeAC(int value, float maximumValue, float *r, float *g, float *b) +{ + int quantR = (int)floorf(value / (19 * 19)); + int quantG = (int)floorf(value / 19) % 19; + int quantB = (int)value % 19; + + *r = signPow(((float)quantR - 9) / 9, 2.0) * maximumValue; + *g = signPow(((float)quantG - 9) / 9, 2.0) * maximumValue; + *b = signPow(((float)quantB - 9) / 9, 2.0) * maximumValue; +} + +int decodeToArray(const char *blurhash, int width, int height, int punch, int nChannels, uint8_t *pixelArray) +{ + if (!isValidBlurhash(blurhash)) { + return -1; + } + if (punch < 1) { + punch = 1; + } + + int sizeFlag = decodeToInt(blurhash, 0, 1); + int numY = (int)floorf(sizeFlag / 9) + 1; + int numX = (sizeFlag % 9) + 1; + int iter = 0; + + float r = 0, g = 0, b = 0; + int quantizedMaxValue = decodeToInt(blurhash, 1, 2); + if (quantizedMaxValue == -1) { + return -1; + } + + float maxValue = ((float)(quantizedMaxValue + 1)) / 166; + + int colors_size = numX * numY; + float colors[colors_size][3]; + + for (iter = 0; iter < colors_size; iter++) { + if (iter == 0) { + int value = decodeToInt(blurhash, 2, 6); + if (value == -1) { + return -1; + } + decodeDC(value, &r, &g, &b); + colors[iter][0] = r; + colors[iter][1] = g; + colors[iter][2] = b; + } else { + int value = decodeToInt(blurhash, 4 + iter * 2, 6 + iter * 2); + if (value == -1) { + return -1; + } + decodeAC(value, maxValue * punch, &r, &g, &b); + colors[iter][0] = r; + colors[iter][1] = g; + colors[iter][2] = b; + } + } + + int bytesPerRow = width * nChannels; + int x = 0, y = 0, i = 0, j = 0; + int intR = 0, intG = 0, intB = 0; + + for (y = 0; y < height; y++) { + for (x = 0; x < width; x++) { + float r = 0, g = 0, b = 0; + + for (j = 0; j < numY; j++) { + for (i = 0; i < numX; i++) { + float basics = cos((M_PI * x * i) / width) * cos((M_PI * y * j) / height); + int idx = i + j * numX; + r += colors[idx][0] * basics; + g += colors[idx][1] * basics; + b += colors[idx][2] * basics; + } + } + + intR = linearTosRGB(r); + intG = linearTosRGB(g); + intB = linearTosRGB(b); + + pixelArray[nChannels * x + 0 + y * bytesPerRow] = clampToUByte(&intR); + pixelArray[nChannels * x + 1 + y * bytesPerRow] = clampToUByte(&intG); + pixelArray[nChannels * x + 2 + y * bytesPerRow] = clampToUByte(&intB); + + if (nChannels == 4) { + pixelArray[nChannels * x + 3 + y * bytesPerRow] = 255; + } + } + } + return 0; +} + +uint8_t *decode(const char *blurhash, int width, int height, int punch, int nChannels) +{ + int bytesPerRow = width * nChannels; + uint8_t *pixelArray = createByteArray(bytesPerRow * height); + + if (decodeToArray(blurhash, width, height, punch, nChannels, pixelArray) == -1) { + return NULL; + } + return pixelArray; +} \ No newline at end of file diff --git a/src/blurhash.h b/src/blurhash.h new file mode 100644 index 000000000..aa5b07114 --- /dev/null +++ b/src/blurhash.h @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2018 Wolt Enterprises +// SPDX-License-Identifiert: MIT + +#pragma once + +#include +#include +#include +#include +#include + +uint8_t *decode(const char *blurhash, int width, int height, int punch, int nChannels); + +bool isValidBlurhash(const char *blurhash); \ No newline at end of file diff --git a/src/blurhashimageprovider.cpp b/src/blurhashimageprovider.cpp new file mode 100644 index 000000000..ed9976268 --- /dev/null +++ b/src/blurhashimageprovider.cpp @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2021 Tobias Fella +// SPDX-License-Identifier: LGPL-2.0-or-later + +#include "blurhashimageprovider.h" + +#include +#include + +#include "blurhash.h" + +BlurhashImageProvider::BlurhashImageProvider() + : QQuickImageProvider(QQuickImageProvider::Image) +{ +} + +QImage BlurhashImageProvider::requestImage(const QString &id, QSize *size, const QSize &requestedSize) +{ + if (id.isEmpty()) { + return QImage(); + } + *size = requestedSize; + if (size->width() == -1) { + size->setWidth(256); + } + if (size->height() == -1) { + size->setHeight(256); + } + auto data = decode(QUrl::fromPercentEncoding(id.toLatin1()).toLatin1().data(), size->width(), size->height(), 1, 3); + QImage image(data, size->width(), size->height(), size->width() * 3, QImage::Format_RGB888, free, data); + return image; +} \ No newline at end of file diff --git a/src/blurhashimageprovider.h b/src/blurhashimageprovider.h new file mode 100644 index 000000000..1be282403 --- /dev/null +++ b/src/blurhashimageprovider.h @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2021 Tobias Fella +// SPDX-License-Identifier: LGPL-2.0-or-later + +#pragma once + +#include + +class BlurhashImageProvider : public QQuickImageProvider +{ +public: + BlurhashImageProvider(); + QImage requestImage(const QString &id, QSize *size, const QSize &requestedSize) override; +}; \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index fc152da3f..f8b41000c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -59,6 +59,7 @@ #include "webshortcutmodel.h" #include "spellcheckhighlighter.h" #include "customemojimodel.h" +#include "blurhashimageprovider.h" #ifdef HAVE_COLORSCHEME #include "colorschemer.h" #endif @@ -212,6 +213,7 @@ int main(int argc, char *argv[]) engine.addImportPath("qrc:/imports"); engine.addImageProvider(QLatin1String("mxc"), new MatrixImageProvider); + engine.addImageProvider(QLatin1String("blurhash"), new BlurhashImageProvider); engine.load(QUrl(QStringLiteral("qrc:/qml/main.qml"))); if (engine.rootObjects().isEmpty()) {