Show inline path thumbnails for stickers.

This commit is contained in:
John Preston 2021-07-02 13:13:48 +03:00
parent f09b91ebb5
commit 4124c2eb57
21 changed files with 355 additions and 38 deletions

View file

@ -779,6 +779,12 @@ void StickerSetBox::Inner::paintSticker(
ppos,
width(),
image->pix(w, h));
} else {
ChatHelpers::PaintStickerThumbnailPath(
p,
media.get(),
QRect(ppos, QSize(w, h)),
st::windowBgRipple->c);
}
}

View file

@ -832,6 +832,13 @@ void FieldAutocomplete::Inner::paintEvent(QPaintEvent *e) {
} else if (const auto image = media->getStickerSmall()) {
QPoint ppos = pos + QPoint((st::stickerPanSize.width() - w) / 2, (st::stickerPanSize.height() - h) / 2);
p.drawPixmapLeft(ppos, width(), image->pix(w, h));
} else {
QPoint ppos = pos + QPoint((st::stickerPanSize.width() - w) / 2, (st::stickerPanSize.height() - h) / 2);
ChatHelpers::PaintStickerThumbnailPath(
p,
media.get(),
QRect(ppos, QSize(w, h)),
st::windowBgRipple->c);
}
}
}

View file

@ -1918,6 +1918,12 @@ void StickersListWidget::paintSticker(Painter &p, Set &set, int y, int section,
if (sticker.savedFrame.isNull()) {
sticker.savedFrame = pixmap;
}
} else {
PaintStickerThumbnailPath(
p,
media.get(),
QRect(ppos, QSize{ w, h }),
st::windowBgRipple->c);
}
}

View file

@ -188,4 +188,27 @@ std::unique_ptr<Lottie::SinglePlayer> LottieThumbnail(
box);
}
bool PaintStickerThumbnailPath(
QPainter &p,
not_null<Data::DocumentMedia*> media,
QRect target,
QColor fg) {
const auto &path = media->thumbnailPath();
const auto dimensions = media->owner()->dimensions;
if (path.isEmpty() || dimensions.isEmpty()) {
return false;
}
p.save();
auto hq = PainterHighQualityEnabler(p);
p.setBrush(fg);
p.setPen(Qt::NoPen);
p.translate(target.topLeft());
p.scale(
target.width() / float64(dimensions.width()),
target.height() / float64(dimensions.height()));
p.drawPath(path);
p.restore();
return true;
}
} // namespace ChatHelpers

View file

@ -71,4 +71,10 @@ enum class StickerLottieSize : uchar {
QSize box,
std::shared_ptr<Lottie::FrameRenderer> renderer = nullptr);
bool PaintStickerThumbnailPath(
QPainter &p,
not_null<Data::DocumentMedia*> media,
QRect target,
QColor fg);
} // namespace ChatHelpers

View file

@ -442,12 +442,17 @@ bool DocumentData::checkWallPaperProperties() {
}
void DocumentData::updateThumbnails(
const QByteArray &inlineThumbnailBytes,
const InlineImageLocation &inlineThumbnail,
const ImageWithLocation &thumbnail,
const ImageWithLocation &videoThumbnail) {
if (!inlineThumbnailBytes.isEmpty()
if (!inlineThumbnail.bytes.isEmpty()
&& _inlineThumbnailBytes.isEmpty()) {
_inlineThumbnailBytes = inlineThumbnailBytes;
_inlineThumbnailBytes = inlineThumbnail.bytes;
if (inlineThumbnail.isPath) {
_flags |= Flag::InlineThumbnailIsPath;
} else {
_flags &= ~Flag::InlineThumbnailIsPath;
}
}
Data::UpdateCloudFile(
_thumbnail,

View file

@ -171,13 +171,16 @@ public:
[[nodiscard]] int videoThumbnailByteSize() const;
void updateThumbnails(
const QByteArray &inlineThumbnailBytes,
const InlineImageLocation &inlineThumbnail,
const ImageWithLocation &thumbnail,
const ImageWithLocation &videoThumbnail);
[[nodiscard]] QByteArray inlineThumbnailBytes() const {
return _inlineThumbnailBytes;
}
[[nodiscard]] bool inlineThumbnailIsPath() const {
return (_flags & Flag::InlineThumbnailIsPath);
}
void clearInlineThumbnailBytes() {
_inlineThumbnailBytes = QByteArray();
}
@ -257,6 +260,7 @@ private:
DownloadCancelled = 0x10,
LoadedInMediaCache = 0x20,
HasAttachedStickers = 0x40,
InlineThumbnailIsPath = 0x80,
};
using Flags = base::flags<Flag>;
friend constexpr bool is_flag_type(Flag) { return true; };

View file

@ -172,7 +172,7 @@ void DocumentMedia::setGoodThumbnail(QImage thumbnail) {
}
Image *DocumentMedia::thumbnailInline() const {
if (!_inlineThumbnail) {
if (!_inlineThumbnail && !_owner->inlineThumbnailIsPath()) {
const auto bytes = _owner->inlineThumbnailBytes();
if (!bytes.isEmpty()) {
auto image = Images::FromInlineBytes(bytes);
@ -186,6 +186,19 @@ Image *DocumentMedia::thumbnailInline() const {
return _inlineThumbnail.get();
}
const QPainterPath &DocumentMedia::thumbnailPath() const {
if (_pathThumbnail.isEmpty() && _owner->inlineThumbnailIsPath()) {
const auto bytes = _owner->inlineThumbnailBytes();
if (!bytes.isEmpty()) {
_pathThumbnail = Images::PathFromInlineBytes(bytes);
if (_pathThumbnail.isEmpty()) {
_owner->clearInlineThumbnailBytes();
}
}
}
return _pathThumbnail;
}
Image *DocumentMedia::thumbnail() const {
return _thumbnail.get();
}

View file

@ -52,6 +52,7 @@ public:
void setGoodThumbnail(QImage thumbnail);
[[nodiscard]] Image *thumbnailInline() const;
[[nodiscard]] const QPainterPath &thumbnailPath() const;
[[nodiscard]] Image *thumbnail() const;
[[nodiscard]] QSize thumbnailSize() const;
@ -102,6 +103,7 @@ private:
const not_null<DocumentData*> _owner;
std::unique_ptr<Image> _goodThumbnail;
mutable std::unique_ptr<Image> _inlineThumbnail;
mutable QPainterPath _pathThumbnail;
std::unique_ptr<Image> _thumbnail;
std::unique_ptr<Image> _sticker;
QByteArray _bytes;

View file

@ -134,18 +134,25 @@ std::vector<UnavailableReason> ExtractUnavailableReasons(
}) | ranges::to_vector;
}
[[nodiscard]] QByteArray FindInlineThumbnail(
[[nodiscard]] InlineImageLocation FindInlineThumbnail(
const QVector<MTPPhotoSize> &sizes) {
const auto i = ranges::find(
sizes,
mtpc_photoStrippedSize,
&MTPPhotoSize::type);
const auto j = ranges::find(
sizes,
mtpc_photoPathSize,
&MTPPhotoSize::type);
return (i != sizes.end())
? i->c_photoStrippedSize().vbytes().v
: QByteArray();
? InlineImageLocation{ i->c_photoStrippedSize().vbytes().v, false }
: (j != sizes.end())
? InlineImageLocation{ j->c_photoPathSize().vbytes().v, true }
: InlineImageLocation();
}
[[nodiscard]] QByteArray FindDocumentInlineThumbnail(const MTPDdocument &data) {
[[nodiscard]] InlineImageLocation FindDocumentInlineThumbnail(
const MTPDdocument &data) {
return FindInlineThumbnail(data.vthumbs().value_or_empty());
}
@ -193,7 +200,8 @@ std::vector<UnavailableReason> ExtractUnavailableReasons(
}
[[nodiscard]] QByteArray FindPhotoInlineThumbnail(const MTPDphoto &data) {
return FindInlineThumbnail(data.vsizes().v);
const auto thumbnail = FindInlineThumbnail(data.vsizes().v);
return !thumbnail.isPath ? thumbnail.bytes : QByteArray();
}
[[nodiscard]] int VideoStartTime(const MTPDvideoSize &data) {
@ -2656,7 +2664,7 @@ not_null<DocumentData*> Session::processDocument(
data.vdate().v,
data.vattributes().v,
qs(data.vmime_type()),
QByteArray(),
InlineImageLocation(),
thumbnail,
ImageWithLocation(),
data.vdc_id().v,
@ -2673,7 +2681,7 @@ not_null<DocumentData*> Session::document(
TimeId date,
const QVector<MTPDocumentAttribute> &attributes,
const QString &mime,
const QByteArray &inlineThumbnailBytes,
const InlineImageLocation &inlineThumbnail,
const ImageWithLocation &thumbnail,
const ImageWithLocation &videoThumbnail,
int32 dc,
@ -2686,7 +2694,7 @@ not_null<DocumentData*> Session::document(
date,
attributes,
mime,
inlineThumbnailBytes,
inlineThumbnail,
thumbnail,
videoThumbnail,
dc,
@ -2754,7 +2762,7 @@ DocumentData *Session::documentFromWeb(
base::unixtime::now(),
data.vattributes().v,
data.vmime_type().v,
QByteArray(),
InlineImageLocation(),
ImageWithLocation{ .location = thumbnailLocation },
ImageWithLocation{ .location = videoThumbnailLocation },
session().mainDcId(),
@ -2776,7 +2784,7 @@ DocumentData *Session::documentFromWeb(
base::unixtime::now(),
data.vattributes().v,
data.vmime_type().v,
QByteArray(),
InlineImageLocation(),
ImageWithLocation{ .location = thumbnailLocation },
ImageWithLocation{ .location = videoThumbnailLocation },
session().mainDcId(),
@ -2796,7 +2804,7 @@ void Session::documentApplyFields(
void Session::documentApplyFields(
not_null<DocumentData*> document,
const MTPDdocument &data) {
const auto inlineThumbnailBytes = FindDocumentInlineThumbnail(data);
const auto inlineThumbnail = FindDocumentInlineThumbnail(data);
const auto thumbnailSize = FindDocumentThumbnail(data);
const auto videoThumbnailSize = FindDocumentVideoThumbnail(data);
const auto prepared = Images::FromPhotoSize(
@ -2813,7 +2821,7 @@ void Session::documentApplyFields(
data.vdate().v,
data.vattributes().v,
qs(data.vmime_type()),
inlineThumbnailBytes,
inlineThumbnail,
prepared,
videoThumbnail,
data.vdc_id().v,
@ -2827,7 +2835,7 @@ void Session::documentApplyFields(
TimeId date,
const QVector<MTPDocumentAttribute> &attributes,
const QString &mime,
const QByteArray &inlineThumbnailBytes,
const InlineImageLocation &inlineThumbnail,
const ImageWithLocation &thumbnail,
const ImageWithLocation &videoThumbnail,
int32 dc,
@ -2838,7 +2846,7 @@ void Session::documentApplyFields(
document->date = date;
document->setMimeString(mime);
document->updateThumbnails(
inlineThumbnailBytes,
inlineThumbnail,
thumbnail,
videoThumbnail);
document->size = size;

View file

@ -476,7 +476,7 @@ public:
TimeId date,
const QVector<MTPDocumentAttribute> &attributes,
const QString &mime,
const QByteArray &inlineThumbnailBytes,
const InlineImageLocation &inlineThumbnail,
const ImageWithLocation &thumbnail,
const ImageWithLocation &videoThumbnail,
int32 dc,
@ -743,7 +743,7 @@ private:
TimeId date,
const QVector<MTPDocumentAttribute> &attributes,
const QString &mime,
const QByteArray &inlineThumbnailBytes,
const InlineImageLocation &inlineThumbnail,
const ImageWithLocation &thumbnail,
const ImageWithLocation &videoThumbnail,
int32 dc,

View file

@ -106,7 +106,7 @@ enum class FilterType {
base::unixtime::now(),
QVector<MTPDocumentAttribute>(),
QString(),
QByteArray(),
InlineImageLocation(),
ImageWithLocation(),
ImageWithLocation(),
owner->session().mainDcId(),

View file

@ -150,11 +150,14 @@ QSize Sticker::GetAnimatedEmojiSize(
void Sticker::draw(Painter &p, const QRect &r, bool selected) {
ensureDataMediaCreated();
paintPath(p, r, selected);
return;
if (readyToDrawLottie()) {
paintLottie(p, r, selected);
} else if (_data->sticker()
&& (!_data->sticker()->animated || !_replacements)) {
paintPixmap(p, r, selected);
} else if (!_data->sticker()
|| (_data->sticker()->animated && _replacements)
|| !paintPixmap(p, r, selected)) {
paintPath(p, r, selected);
}
}
@ -215,15 +218,25 @@ void Sticker::paintLottie(Painter &p, const QRect &r, bool selected) {
}
}
void Sticker::paintPixmap(Painter &p, const QRect &r, bool selected) {
bool Sticker::paintPixmap(Painter &p, const QRect &r, bool selected) {
const auto pixmap = paintedPixmap(selected);
if (!pixmap.isNull()) {
p.drawPixmap(
QPoint(
r.x() + (r.width() - _size.width()) / 2,
r.y() + (r.height() - _size.height()) / 2),
pixmap);
if (pixmap.isNull()) {
return false;
}
p.drawPixmap(
QPoint(
r.x() + (r.width() - _size.width()) / 2,
r.y() + (r.height() - _size.height()) / 2),
pixmap);
return true;
}
void Sticker::paintPath(Painter &p, const QRect &r, bool selected) {
ChatHelpers::PaintStickerThumbnailPath(
p,
_dataMedia.get(),
r,
(selected ? st::msgServiceBgSelected : st::msgServiceBg)->c);
}
QPixmap Sticker::paintedPixmap(bool selected) const {
@ -306,7 +319,9 @@ void Sticker::dataMediaCreated() const {
Expects(_dataMedia != nullptr);
_dataMedia->goodThumbnailWanted();
_dataMedia->thumbnailWanted(_parent->data()->fullId());
if (_dataMedia->thumbnailPath().isEmpty()) {
_dataMedia->thumbnailWanted(_parent->data()->fullId());
}
_parent->history()->owner().registerHeavyViewPart(_parent);
}

View file

@ -84,7 +84,8 @@ public:
private:
[[nodiscard]] bool isEmojiSticker() const;
void paintLottie(Painter &p, const QRect &r, bool selected);
void paintPixmap(Painter &p, const QRect &r, bool selected);
bool paintPixmap(Painter &p, const QRect &r, bool selected);
void paintPath(Painter &p, const QRect &r, bool selected);
[[nodiscard]] QPixmap paintedPixmap(bool selected) const;
void ensureDataMediaCreated() const;

View file

@ -14,7 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_photo_media.h"
#include "data/data_document_media.h"
#include "data/stickers/data_stickers.h"
#include "chat_helpers/gifs_list_widget.h" // ChatHelpers::AddGifAction
#include "chat_helpers/gifs_list_widget.h" // ChatHelpers::AddGifAction.
#include "chat_helpers/stickers_lottie.h"
#include "inline_bots/inline_bot_result.h"
#include "lottie/lottie_single_player.h"
@ -483,6 +483,19 @@ void Sticker::paint(Painter &p, const QRect &clip, const PaintContext *context)
int w = _thumb.width() / cIntRetinaFactor(), h = _thumb.height() / cIntRetinaFactor();
QPoint pos = QPoint((st::stickerPanSize.width() - w) / 2, (st::stickerPanSize.height() - h) / 2);
p.drawPixmap(pos, _thumb);
} else {
const auto thumbSize = getThumbSize();
const auto w = thumbSize.width();
const auto h = thumbSize.height();
ChatHelpers::PaintStickerThumbnailPath(
p,
_dataMedia.get(),
QRect(
(st::stickerPanSize.width() - w) / 2,
(st::stickerPanSize.height() - h) / 2,
w,
h),
st::windowBgRipple->c);
}
}

View file

@ -164,7 +164,7 @@ DocumentData *Document::readFromStreamHelper(
date,
attributes,
mime,
QByteArray(),
InlineImageLocation(),
ImageWithLocation{
.location = *thumb,
.bytesCount = thumbnailByteSize

View file

@ -1995,7 +1995,7 @@ void Account::importOldRecentStickers() {
date,
attributes,
mime,
QByteArray(),
InlineImageLocation(),
ImageWithLocation(),
ImageWithLocation(),
dc,

View file

@ -98,7 +98,7 @@ void LoadAndApplyThumbnail(
};
document->updateThumbnails(
QByteArray(),
InlineImageLocation(),
imageWithLocation,
ImageWithLocation{ .location = ImageLocation() });

View file

@ -93,6 +93,206 @@ QImage FromInlineBytes(const QByteArray &bytes) {
return App::readImage(ExpandInlineBytes(bytes));
}
// Thanks TDLib for code.
QByteArray ExpandPathInlineBytes(const QByteArray &bytes) {
auto result = QByteArray();
result.reserve(3 * (bytes.size() + 1));
result.append('M');
for (unsigned char c : bytes) {
if (c >= 128 + 64) {
result.append("AACAAAAHAAALMAAAQASTAVAAAZ"
"aacaaaahaaalmaaaqastava.az0123456789-,"[c - 128 - 64]);
} else {
if (c >= 128) {
result.append(',');
} else if (c >= 64) {
result.append('-');
}
char buffer[3] = { 0 };
std::to_chars(buffer, buffer + 3, (c & 63));
result.append(buffer);
}
}
result.append('z');
return result;
}
QPainterPath PathFromInlineBytes(const QByteArray &bytes) {
if (bytes.isEmpty()) {
return QPainterPath();
}
const auto expanded = ExpandPathInlineBytes(bytes);
const auto path = expanded.data(); // Allows checking for '\0' by index.
auto position = 0;
const auto isAlpha = [](char c) {
c |= 0x20;
return 'a' <= c && c <= 'z';
};
const auto isDigit = [](char c) {
return '0' <= c && c <= '9';
};
const auto skipCommas = [&] {
while (path[position] == ',') {
++position;
}
};
const auto getNumber = [&] {
skipCommas();
auto sign = 1;
if (path[position] == '-') {
sign = -1;
++position;
}
double res = 0;
while (isDigit(path[position])) {
res = res * 10 + path[position++] - '0';
}
if (path[position] == '.') {
++position;
double mul = 0.1;
while (isDigit(path[position])) {
res += (path[position] - '0') * mul;
mul *= 0.1;
++position;
}
}
return sign * res;
};
auto result = QPainterPath();
auto x = 0.;
auto y = 0.;
while (path[position] != '\0') {
skipCommas();
if (path[position] == '\0') {
break;
}
while (path[position] == 'm' || path[position] == 'M') {
auto command = path[position++];
do {
if (command == 'm') {
x += getNumber();
y += getNumber();
} else {
x = getNumber();
y = getNumber();
}
skipCommas();
} while (path[position] != '\0' && !isAlpha(path[position]));
}
auto xStart = x;
auto yStart = y;
result.moveTo(xStart, yStart);
auto haveLastEndControlPoint = false;
auto xLastEndControlPoint = 0.;
auto yLastEndControlPoint = 0.;
auto isClosed = false;
auto command = '-';
while (!isClosed) {
skipCommas();
if (path[position] == '\0') {
LOG(("SVG Error: Receive unclosed path: %1"
).arg(QString::fromLatin1(path)));
return QPainterPath();
}
if (isAlpha(path[position])) {
command = path[position++];
}
switch (command) {
case 'l':
case 'L':
case 'h':
case 'H':
case 'v':
case 'V':
if (command == 'l' || command == 'h') {
x += getNumber();
} else if (command == 'L' || command == 'H') {
x = getNumber();
}
if (command == 'l' || command == 'v') {
y += getNumber();
} else if (command == 'L' || command == 'V') {
y = getNumber();
}
result.lineTo(x, y);
haveLastEndControlPoint = false;
break;
case 'C':
case 'c':
case 'S':
case 's': {
auto xStartControlPoint = 0.;
auto yStartControlPoint = 0.;
if (command == 'S' || command == 's') {
if (haveLastEndControlPoint) {
xStartControlPoint = 2 * x - xLastEndControlPoint;
yStartControlPoint = 2 * y - yLastEndControlPoint;
} else {
xStartControlPoint = x;
yStartControlPoint = y;
}
} else {
xStartControlPoint = getNumber();
yStartControlPoint = getNumber();
if (command == 'c') {
xStartControlPoint += x;
yStartControlPoint += y;
}
}
xLastEndControlPoint = getNumber();
yLastEndControlPoint = getNumber();
if (command == 'c' || command == 's') {
xLastEndControlPoint += x;
yLastEndControlPoint += y;
}
haveLastEndControlPoint = true;
if (command == 'c' || command == 's') {
x += getNumber();
y += getNumber();
} else {
x = getNumber();
y = getNumber();
}
result.cubicTo(
xStartControlPoint,
yStartControlPoint,
xLastEndControlPoint,
yLastEndControlPoint,
x,
y);
break;
}
case 'm':
case 'M':
--position;
[[fallthrough]];
case 'z':
case 'Z':
if (x != xStart || y != yStart) {
x = xStart;
y = yStart;
result.lineTo(x, y);
}
isClosed = true;
break;
default:
LOG(("SVG Error: Receive invalid command %1 at pos %2: %3"
).arg(command
).arg(position
).arg(QString::fromLatin1(path)));
return QPainterPath();
}
}
}
return result;
}
} // namespace Images
Image::Image(const QString &path) : Image(ReadContent(path)) {

View file

@ -9,10 +9,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/image/image_prepare.h"
class QPainterPath;
namespace Images {
[[nodiscard]] QByteArray ExpandInlineBytes(const QByteArray &bytes);
[[nodiscard]] QImage FromInlineBytes(const QByteArray &bytes);
[[nodiscard]] QPainterPath PathFromInlineBytes(const QByteArray &bytes);
} // namespace Images

View file

@ -620,6 +620,11 @@ struct ImageWithLocation {
int progressivePartSize = 0;
};
struct InlineImageLocation {
QByteArray bytes;
bool isPath = false;
};
InMemoryKey inMemoryKey(const StorageFileLocation &location);
inline InMemoryKey inMemoryKey(const StorageImageLocation &location) {