Cache default SpoilerMess on disk.

This commit is contained in:
John Preston 2022-09-09 11:51:08 +04:00
parent 00a668c3f3
commit c732bd874f
2 changed files with 262 additions and 5 deletions

View file

@ -7,12 +7,39 @@
#include "ui/effects/spoiler_mess.h"
#include "ui/painter.h"
#include "ui/integration.h"
#include "base/random.h"
#include <QtCore/QBuffer>
#include <QtCore/QFile>
#include <QtCore/QDir>
#include <crl/crl_async.h>
#include <xxhash.h>
namespace Ui {
namespace {
constexpr auto kVersion = 1;
constexpr auto kFramesPerRow = 10;
constexpr auto kImageSpoilerDarkenAlpha = 32;
constexpr auto kMaxCacheSize = 5 * 1024 * 1024;
constexpr auto kDefaultFrameDuration = crl::time(33);
constexpr auto kDefaultFramesCount = 60;
constexpr auto kDefaultCanvasSize = 100;
std::atomic<const SpoilerMessCached*> DefaultMask/* = nullptr*/;
std::condition_variable *DefaultMaskSignal/* = nullptr*/;
std::mutex *DefaultMaskMutex/* = nullptr*/;
struct Header {
uint32 version = 0;
uint32 dataLength = 0;
uint32 dataHash = 0;
int32 framesCount = 0;
int32 canvasSize = 0;
int32 frameDuration = 0;
};
struct Particle {
crl::time start = 0;
@ -64,6 +91,39 @@ struct Particle {
return result;
}
[[nodiscard]] QString DefaultMaskCacheFolder() {
const auto base = Integration::Instance().emojiCacheFolder();
return base.isEmpty() ? QString() : (base + "/spoiler");
}
[[nodiscard]] QString DefaultMaskCachePath(const QString &folder) {
return folder + "/mask";
}
[[nodiscard]] std::optional<SpoilerMessCached> ReadDefaultMask(
std::optional<SpoilerMessCached::Validator> validator) {
const auto folder = DefaultMaskCacheFolder();
if (folder.isEmpty()) {
return {};
}
auto file = QFile(DefaultMaskCachePath(folder));
return (file.open(QIODevice::ReadOnly) && file.size() <= kMaxCacheSize)
? SpoilerMessCached::FromSerialized(file.readAll(), validator)
: std::nullopt;
}
void WriteDefaultMask(const SpoilerMessCached &mask) {
const auto folder = DefaultMaskCacheFolder();
if (!QDir().mkpath(folder)) {
return;
}
const auto bytes = mask.serialize();
auto file = QFile(DefaultMaskCachePath(folder));
if (file.open(QIODevice::WriteOnly) && bytes.size() <= kMaxCacheSize) {
file.write(bytes);
}
}
} // namespace
SpoilerMessCached GenerateSpoilerMess(
@ -173,6 +233,16 @@ SpoilerMessCached::SpoilerMessCached(
((_framesCount + kFramesPerRow - 1) / kFramesPerRow) * _canvasSize));
}
SpoilerMessCached::SpoilerMessCached(
const SpoilerMessCached &mask,
const QColor &color)
: SpoilerMessCached(
style::colorizeImage(*mask.frame(0).image, color),
mask.framesCount(),
mask.frameDuration(),
mask.canvasSize()) {
}
SpoilerMessFrame SpoilerMessCached::frame(int index) const {
const auto row = index / kFramesPerRow;
const auto column = index - row * kFramesPerRow;
@ -190,13 +260,185 @@ SpoilerMessFrame SpoilerMessCached::frame() const {
return frame((crl::now() / _frameDuration) % _framesCount);
}
crl::time SpoilerMessCached::frameDuration() const {
return _frameDuration;
}
int SpoilerMessCached::framesCount() const {
return _framesCount;
}
int SpoilerMessCached::canvasSize() const {
return _canvasSize;
}
QByteArray SpoilerMessCached::serialize() const {
return QByteArray();
Expects(_frameDuration < std::numeric_limits<int32>::max());
const auto skip = sizeof(Header);
auto result = QByteArray(skip, Qt::Uninitialized);
auto header = Header{
.version = kVersion,
.framesCount = _framesCount,
.canvasSize = _canvasSize,
.frameDuration = int32(_frameDuration),
};
const auto width = int(_image.width());
const auto height = int(_image.height());
auto grayscale = QImage(width, height, QImage::Format_Grayscale8);
{
auto tobytes = grayscale.bits();
auto frombytes = _image.constBits();
const auto toadd = grayscale.bytesPerLine() - width;
const auto fromadd = _image.bytesPerLine() - (width * 4);
for (auto y = 0; y != height; ++y) {
for (auto x = 0; x != width; ++x) {
*tobytes++ = *frombytes;
frombytes += 4;
}
tobytes += toadd;
frombytes += fromadd;
}
}
auto device = QBuffer(&result);
device.open(QIODevice::WriteOnly);
device.seek(skip);
grayscale.save(&device, "PNG");
device.close();
header.dataLength = result.size() - skip;
header.dataHash = XXH32(result.data() + skip, header.dataLength, 0);
memcpy(result.data(), &header, skip);
return result;
}
std::optional<SpoilerMessCached> SpoilerMessCached::FromSerialized(
const QByteArray &data) {
return std::nullopt;
QByteArray data,
std::optional<Validator> validator) {
const auto skip = sizeof(Header);
const auto length = data.size();
const auto bytes = reinterpret_cast<const uchar*>(data.constData());
if (length <= skip) {
return {};
}
auto header = Header();
memcpy(&header, bytes, skip);
if (header.version != kVersion
|| header.canvasSize <= 0
|| header.framesCount <= 0
|| header.frameDuration <= 0
|| (validator
&& (validator->frameDuration != header.frameDuration
|| validator->framesCount != header.framesCount
|| validator->canvasSize != header.canvasSize))
|| (skip + header.dataLength != length)
|| (XXH32(bytes + skip, header.dataLength, 0) != header.dataHash)) {
return {};
}
auto grayscale = QImage();
if (!grayscale.loadFromData(bytes + skip, header.dataLength, "PNG")
|| (grayscale.format() != QImage::Format_Grayscale8)) {
return {};
}
const auto count = header.framesCount;
const auto rows = (count + kFramesPerRow - 1) / kFramesPerRow;
const auto columns = std::min(count, kFramesPerRow);
const auto width = grayscale.width();
const auto height = grayscale.height();
if (QSize(width, height) != QSize(columns, rows) * header.canvasSize) {
return {};
}
auto image = QImage(width, height, QImage::Format_ARGB32_Premultiplied);
{
Assert(image.bytesPerLine() % 4 == 0);
auto toints = reinterpret_cast<uint32*>(image.bits());
auto frombytes = grayscale.constBits();
const auto toadd = (image.bytesPerLine() / 4) - width;
const auto fromadd = grayscale.bytesPerLine() - width;
for (auto y = 0; y != height; ++y) {
for (auto x = 0; x != width; ++x) {
const auto byte = uint32(*frombytes++);
*toints++ = (byte << 24) | (byte << 16) | (byte << 8) | byte;
}
toints += toadd;
frombytes += fromadd;
}
}
return SpoilerMessCached(
std::move(image),
count,
header.frameDuration,
header.canvasSize);
}
void PrepareDefaultSpoilerMess() {
DefaultMaskSignal = new std::condition_variable();
DefaultMaskMutex = new std::mutex();
crl::async([] {
const auto ratio = style::DevicePixelRatio();
const auto size = style::ConvertScale(kDefaultCanvasSize) * ratio;
auto cached = ReadDefaultMask(SpoilerMessCached::Validator{
.frameDuration = kDefaultFrameDuration,
.framesCount = kDefaultFramesCount,
.canvasSize = size,
});
if (cached) {
DefaultMask = new SpoilerMessCached(std::move(*cached));
} else {
DefaultMask = new SpoilerMessCached(GenerateSpoilerMess({
.particleFadeInDuration = 200,
.particleFadeOutDuration = 200,
.particleSizeMin = style::ConvertScaleExact(1.5) * ratio,
.particleSizeMax = style::ConvertScaleExact(2.) * ratio,
.particleSpritesCount = 5,
.particlesCount = 2000,
.canvasSize = size,
.framesCount = kDefaultFramesCount,
.frameDuration = kDefaultFrameDuration,
}));
}
auto lock = std::unique_lock(*DefaultMaskMutex);
DefaultMaskSignal->notify_all();
if (!cached) {
WriteDefaultMask(*DefaultMask);
}
});
}
const SpoilerMessCached &DefaultSpoilerMask() {
if (const auto result = DefaultMask.load()) {
return *result;
}
Assert(DefaultMaskSignal != nullptr);
Assert(DefaultMaskMutex != nullptr);
while (true) {
auto lock = std::unique_lock(*DefaultMaskMutex);
if (const auto result = DefaultMask.load()) {
return *result;
}
DefaultMaskSignal->wait(lock);
}
}
const SpoilerMessCached &DefaultImageSpoiler() {
static const auto result = [&] {
const auto mask = Ui::DefaultSpoilerMask();
const auto frame = mask.frame(0);
auto image = QImage(
frame.image->size(),
QImage::Format_ARGB32_Premultiplied);
image.fill(QColor(0, 0, 0, kImageSpoilerDarkenAlpha));
auto p = QPainter(&image);
p.drawImage(0, 0, *frame.image);
p.end();
return Ui::SpoilerMessCached(
std::move(image),
mask.framesCount(),
mask.frameDuration(),
mask.canvasSize());
}();
return result;
}
} // namespace Ui

View file

@ -34,14 +34,25 @@ public:
QImage image,
int framesCount,
crl::time frameDuration,
int size);
int canvasSize);
SpoilerMessCached(const SpoilerMessCached &mask, const QColor &color);
[[nodiscard]] SpoilerMessFrame frame(int index) const;
[[nodiscard]] SpoilerMessFrame frame() const; // Current by time.
[[nodiscard]] crl::time frameDuration() const;
[[nodiscard]] int framesCount() const;
[[nodiscard]] int canvasSize() const;
struct Validator {
crl::time frameDuration = 0;
int framesCount = 0;
int canvasSize = 0;
};
[[nodiscard]] QByteArray serialize() const;
[[nodiscard]] static std::optional<SpoilerMessCached> FromSerialized(
const QByteArray &data);
QByteArray data,
std::optional<Validator> validator = {});
private:
QImage _image;
@ -54,4 +65,8 @@ private:
[[nodiscard]] SpoilerMessCached GenerateSpoilerMess(
const SpoilerMessDescriptor &descriptor);
void PrepareDefaultSpoilerMess();
[[nodiscard]] const SpoilerMessCached &DefaultSpoilerMask();
[[nodiscard]] const SpoilerMessCached &DefaultImageSpoiler();
} // namespace Ui