Show blurhashes for image events

This commit is contained in:
Tobias Fella
2021-08-21 00:19:07 +02:00
parent dada3e300b
commit 046e823d1b
10 changed files with 291 additions and 8 deletions

19
LICENSES/MIT.txt Executable file
View File

@@ -0,0 +1,19 @@
MIT License Copyright (c) <year> <copyright holders>
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.

View File

@@ -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 {

View File

@@ -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

View File

@@ -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()
}
}
}

View File

@@ -34,6 +34,8 @@ add_executable(neochat
commandmodel.cpp
webshortcutmodel.cpp
spellcheckhighlighter.cpp
blurhash.cpp
blurhashimageprovider.cpp
../res.qrc
)

186
src/blurhash.cpp Normal file
View File

@@ -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;
}

14
src/blurhash.h Normal file
View File

@@ -0,0 +1,14 @@
// SPDX-FileCopyrightText: 2018 Wolt Enterprises
// SPDX-License-Identifiert: MIT
#pragma once
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
uint8_t *decode(const char *blurhash, int width, int height, int punch, int nChannels);
bool isValidBlurhash(const char *blurhash);

View File

@@ -0,0 +1,31 @@
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "blurhashimageprovider.h"
#include <QImage>
#include <QString>
#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;
}

View File

@@ -0,0 +1,13 @@
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once
#include <QQuickImageProvider>
class BlurhashImageProvider : public QQuickImageProvider
{
public:
BlurhashImageProvider();
QImage requestImage(const QString &id, QSize *size, const QSize &requestedSize) override;
};

View File

@@ -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()) {