Cache default SpoilerMess on disk.
This commit is contained in:
parent
00a668c3f3
commit
c732bd874f
2 changed files with 262 additions and 5 deletions
|
|
@ -7,12 +7,39 @@
|
||||||
#include "ui/effects/spoiler_mess.h"
|
#include "ui/effects/spoiler_mess.h"
|
||||||
|
|
||||||
#include "ui/painter.h"
|
#include "ui/painter.h"
|
||||||
|
#include "ui/integration.h"
|
||||||
#include "base/random.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 Ui {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
constexpr auto kVersion = 1;
|
||||||
constexpr auto kFramesPerRow = 10;
|
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 {
|
struct Particle {
|
||||||
crl::time start = 0;
|
crl::time start = 0;
|
||||||
|
|
@ -64,6 +91,39 @@ struct Particle {
|
||||||
return result;
|
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
|
} // namespace
|
||||||
|
|
||||||
SpoilerMessCached GenerateSpoilerMess(
|
SpoilerMessCached GenerateSpoilerMess(
|
||||||
|
|
@ -173,6 +233,16 @@ SpoilerMessCached::SpoilerMessCached(
|
||||||
((_framesCount + kFramesPerRow - 1) / kFramesPerRow) * _canvasSize));
|
((_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 {
|
SpoilerMessFrame SpoilerMessCached::frame(int index) const {
|
||||||
const auto row = index / kFramesPerRow;
|
const auto row = index / kFramesPerRow;
|
||||||
const auto column = index - row * kFramesPerRow;
|
const auto column = index - row * kFramesPerRow;
|
||||||
|
|
@ -190,13 +260,185 @@ SpoilerMessFrame SpoilerMessCached::frame() const {
|
||||||
return frame((crl::now() / _frameDuration) % _framesCount);
|
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 {
|
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(
|
std::optional<SpoilerMessCached> SpoilerMessCached::FromSerialized(
|
||||||
const QByteArray &data) {
|
QByteArray data,
|
||||||
return std::nullopt;
|
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
|
} // namespace Ui
|
||||||
|
|
|
||||||
|
|
@ -34,14 +34,25 @@ public:
|
||||||
QImage image,
|
QImage image,
|
||||||
int framesCount,
|
int framesCount,
|
||||||
crl::time frameDuration,
|
crl::time frameDuration,
|
||||||
int size);
|
int canvasSize);
|
||||||
|
SpoilerMessCached(const SpoilerMessCached &mask, const QColor &color);
|
||||||
|
|
||||||
[[nodiscard]] SpoilerMessFrame frame(int index) const;
|
[[nodiscard]] SpoilerMessFrame frame(int index) const;
|
||||||
[[nodiscard]] SpoilerMessFrame frame() const; // Current by time.
|
[[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]] QByteArray serialize() const;
|
||||||
[[nodiscard]] static std::optional<SpoilerMessCached> FromSerialized(
|
[[nodiscard]] static std::optional<SpoilerMessCached> FromSerialized(
|
||||||
const QByteArray &data);
|
QByteArray data,
|
||||||
|
std::optional<Validator> validator = {});
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QImage _image;
|
QImage _image;
|
||||||
|
|
@ -54,4 +65,8 @@ private:
|
||||||
[[nodiscard]] SpoilerMessCached GenerateSpoilerMess(
|
[[nodiscard]] SpoilerMessCached GenerateSpoilerMess(
|
||||||
const SpoilerMessDescriptor &descriptor);
|
const SpoilerMessDescriptor &descriptor);
|
||||||
|
|
||||||
|
void PrepareDefaultSpoilerMess();
|
||||||
|
[[nodiscard]] const SpoilerMessCached &DefaultSpoilerMask();
|
||||||
|
[[nodiscard]] const SpoilerMessCached &DefaultImageSpoiler();
|
||||||
|
|
||||||
} // namespace Ui
|
} // namespace Ui
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue