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/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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue