diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 56e001549..c81fb94df 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -12,8 +12,6 @@ add_library(neochat STATIC models/userdirectorylistmodel.h notificationsmanager.cpp notificationsmanager.h - blurhash.cpp - blurhash.h blurhashimageprovider.cpp blurhashimageprovider.h windowcontroller.cpp diff --git a/src/app/blurhash.cpp b/src/app/blurhash.cpp deleted file mode 100644 index f58270a52..000000000 --- a/src/app/blurhash.cpp +++ /dev/null @@ -1,198 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Wolt Enterprises -// SPDX-License-Identifier: MIT - -#include "blurhash.h" - -#include -#include -#include -#include -#include - -namespace -{ -const char chars[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~"; - -struct Color { - float r = 0; - float g = 0; - float b = 0; -}; - -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; -} - -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); -} - -inline float signPow(float value, float exp) -{ - return copysignf(powf(fabsf(value), exp), value); -} - -inline uint8_t clampToUByte(int *src) -{ - if (*src >= 0 && *src <= 255) { - return *src; - } - return (*src < 0) ? 0 : 255; -} - -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; -} - -void decodeDC(int value, Color *color) -{ - color->r = sRGBToLinear(value >> 16); - color->g = sRGBToLinear((value >> 8) & 255); - color->b = sRGBToLinear(value & 255); -} - -void decodeAC(int value, float maximumValue, Color *color) -{ - int quantR = (int)floorf(value / (19 * 19)); - int quantG = (int)floorf(value / 19) % 19; - int quantB = (int)value % 19; - - color->r = signPow(((float)quantR - 9) / 9, 2.0) * maximumValue; - color->g = signPow(((float)quantG - 9) / 9, 2.0) * maximumValue; - color->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; - - Color color; - int quantizedMaxValue = decodeToInt(blurhash, 1, 2); - if (quantizedMaxValue == -1) { - return -1; - } - - const float maxValue = ((float)(quantizedMaxValue + 1)) / 166; - - const int colors_size = numX * numY; - - std::vector colors(colors_size, {0, 0, 0}); - - for (iter = 0; iter < colors_size; iter++) { - if (iter == 0) { - int value = decodeToInt(blurhash, 2, 6); - if (value == -1) { - return -1; - } - decodeDC(value, &color); - colors[iter] = color; - } else { - int value = decodeToInt(blurhash, 4 + iter * 2, 6 + iter * 2); - if (value == -1) { - return -1; - } - decodeAC(value, maxValue * punch, &color); - colors[iter] = color; - } - } - - 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].r * basics; - g += colors[idx].g * basics; - b += colors[idx].b * 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 nullptr; - } - return pixelArray; -} - -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; -} diff --git a/src/app/blurhash.h b/src/app/blurhash.h deleted file mode 100644 index f41fcaaf1..000000000 --- a/src/app/blurhash.h +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Wolt Enterprises -// SPDX-License-Identifier: MIT - -#pragma once - -#include - -/** - * @brief Returns the pixel array of the result image given the blurhash string. - * - * @param blurhash a string representing the blurhash to be decoded. - * @param width the width of the resulting image. - * @param height the height of the resulting image. - * @param punch the factor to improve the contrast, default = 1. - * @param nChannels the number of channels in the resulting image array, 3 = RGB, 4 = RGBA. - * - * @return A pointer to memory region where pixels are stored in (H, W, C) format. - */ -uint8_t *decode(const char *blurhash, int width, int height, int punch, int nChannels); - -/** - * @brief Checks if the Blurhash is valid or not. - * - * @param blurhash a string representing the blurhash. - * - * @return A bool (true if it is a valid blurhash, else false). - */ -bool isValidBlurhash(const char *blurhash); diff --git a/src/app/blurhashimageprovider.cpp b/src/app/blurhashimageprovider.cpp index eb6190437..2e59fcd0d 100644 --- a/src/app/blurhashimageprovider.cpp +++ b/src/app/blurhashimageprovider.cpp @@ -1,31 +1,97 @@ -// SPDX-FileCopyrightText: 2021 Tobias Fella -// SPDX-License-Identifier: LGPL-2.0-or-later +// SPDX-FileCopyrightText: 2024 Joshua Goins +// SPDX-License-Identifier: MIT #include "blurhashimageprovider.h" -#include -#include +#include -#include "blurhash.h" +/* + * Qt unfortunately re-encodes the base83 string in QML. + * The only special ASCII characters used in the blurhash base83 string are: + * #$%*+,-.:;=?@[]^_{|}~ + * QUrl::fromPercentEncoding is too greedy, and spits out invalid characters + * for parts of valid base83 like %14. + */ +// clang-format off +static const QMap knownEncodings = { + {QLatin1String("%23A"), QLatin1String(":")}, + {QLatin1String("%3F"), QLatin1String("?")}, + {QLatin1String("%23"), QLatin1String("#")}, + {QLatin1String("%5B"), QLatin1String("[")}, + {QLatin1String("%5D"), QLatin1String("]")}, + {QLatin1String("%40"), QLatin1String("@")}, + {QLatin1String("%24"), QLatin1String("$")}, + {QLatin1String("%2A"), QLatin1String("*")}, + {QLatin1String("%2B"), QLatin1String("+")}, + {QLatin1String("%2C"), QLatin1String(",")}, + {QLatin1String("%2D"), QLatin1String("-")}, + {QLatin1String("%2E"), QLatin1String(".")}, + {QLatin1String("%3D"), QLatin1String("=")}, + {QLatin1String("%25"), QLatin1String("%")}, + {QLatin1String("%5E"), QLatin1String("^")}, + {QLatin1String("%7C"), QLatin1String("|")}, + {QLatin1String("%7B"), QLatin1String("{")}, + {QLatin1String("%7D"), QLatin1String("}")}, + {QLatin1String("%7E"), QLatin1String("~")}, +}; +// clang-format on -BlurhashImageProvider::BlurhashImageProvider() - : QQuickImageProvider(QQuickImageProvider::Image) +class AsyncImageResponseRunnable : public QObject, public QRunnable { + Q_OBJECT + +Q_SIGNALS: + void done(QImage image); + +public: + AsyncImageResponseRunnable(const QString &id, const QSize &requestedSize) + : m_id(id) + , m_requestedSize(requestedSize) + { + if (m_requestedSize.width() == -1) + m_requestedSize.setWidth(64); + if (m_requestedSize.height() == -1) + m_requestedSize.setHeight(64); + } + + void run() override + { + if (m_id.isEmpty()) + return; + + QString decodedId = m_id; + for (auto i = knownEncodings.constBegin(); i != knownEncodings.constEnd(); ++i) + decodedId.replace(i.key(), i.value()); + + Q_EMIT done(Quotient::BlurHash::decode(decodedId, m_requestedSize)); + } + +private: + QString m_id; + QSize m_requestedSize; +}; + +AsyncImageResponse::AsyncImageResponse(const QString &id, const QSize &requestedSize, QThreadPool *pool) +{ + const auto runnable = new AsyncImageResponseRunnable(id, requestedSize); + connect(runnable, &AsyncImageResponseRunnable::done, this, &AsyncImageResponse::handleDone); + pool->start(runnable); } -QImage BlurhashImageProvider::requestImage(const QString &id, QSize *size, const QSize &requestedSize) +void AsyncImageResponse::handleDone(QImage image) { - 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 + m_image = std::move(image); + Q_EMIT finished(); +} + +QQuickTextureFactory *AsyncImageResponse::textureFactory() const +{ + return QQuickTextureFactory::textureFactoryForImage(m_image); +} + +QQuickImageResponse *BlurHashImageProvider::requestImageResponse(const QString &id, const QSize &requestedSize) +{ + return new AsyncImageResponse(id, requestedSize, &pool); +} + +#include "blurhashimageprovider.moc" diff --git a/src/app/blurhashimageprovider.h b/src/app/blurhashimageprovider.h index 115c98f66..64ca4e688 100644 --- a/src/app/blurhashimageprovider.h +++ b/src/app/blurhashimageprovider.h @@ -1,26 +1,25 @@ -// SPDX-FileCopyrightText: 2021 Tobias Fella -// SPDX-License-Identifier: LGPL-2.0-or-later +// SPDX-FileCopyrightText: 2024 Joshua Goins +// SPDX-License-Identifier: MIT #pragma once -#include +#include +#include -/** - * @class BlurhashImageProvider - * - * A QQuickImageProvider for blurhashes. - * - * @sa QQuickImageProvider - */ -class BlurhashImageProvider : public QQuickImageProvider +class AsyncImageResponse final : public QQuickImageResponse { public: - BlurhashImageProvider(); - - /** - * @brief Return an image for a given ID. - * - * @sa QQuickImageProvider::requestImage - */ - QImage requestImage(const QString &id, QSize *size, const QSize &requestedSize) override; + AsyncImageResponse(const QString &id, const QSize &requestedSize, QThreadPool *pool); + void handleDone(QImage image); + QQuickTextureFactory *textureFactory() const override; + QImage m_image; +}; + +class BlurHashImageProvider : public QQuickAsyncImageProvider +{ +public: + QQuickImageResponse *requestImageResponse(const QString &id, const QSize &requestedSize) override; + +private: + QThreadPool pool; }; diff --git a/src/app/main.cpp b/src/app/main.cpp index 3165d63fc..bd4e5e859 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -292,7 +292,7 @@ int main(int argc, char *argv[]) ShareHandler::instance().setText(parser.value(shareOption)); } - engine.addImageProvider(u"blurhash"_s, new BlurhashImageProvider); + engine.addImageProvider(u"blurhash"_s, new BlurHashImageProvider); engine.loadFromModule("org.kde.neochat", "Main"); diff --git a/src/libneochat/neochatroom.cpp b/src/libneochat/neochatroom.cpp index c417af11a..ee708fd76 100644 --- a/src/libneochat/neochatroom.cpp +++ b/src/libneochat/neochatroom.cpp @@ -18,6 +18,7 @@ #include #include +#include #include #include #include @@ -241,7 +242,7 @@ QCoro::Task NeoChatRoom::doUploadFile(QUrl url, QString body, std::optiona EventContent::FileContentBase *content; if (mime.name().startsWith("image/"_L1)) { QImage image(url.toLocalFile()); - content = new EventContent::ImageContent(url, fileInfo.size(), mime, image.size(), fileInfo.fileName()); + content = new EventContent::ImageContent(url, fileInfo.size(), mime, image.size(), fileInfo.fileName(), BlurHash::encode(image)); } else if (mime.name().startsWith("audio/"_L1)) { content = new EventContent::AudioContent(url, fileInfo.size(), mime, fileInfo.fileName()); } else if (mime.name().startsWith("video/"_L1)) {