tdesktop/Telegram/SourceFiles/data/data_document_media.cpp

556 lines
15 KiB
C++

/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "data/data_document_media.h"
#include "data/data_document.h"
#include "data/data_document_resolver.h"
#include "data/data_session.h"
#include "data/data_cloud_themes.h"
#include "data/data_file_origin.h"
#include "data/data_auto_download.h"
#include "media/clip/media_clip_reader.h"
#include "main/main_session.h"
#include "main/main_session_settings.h"
#include "lottie/lottie_animation.h"
#include "lottie/lottie_frame_generator.h"
#include "ffmpeg/ffmpeg_frame_generator.h"
#include "history/history_item.h"
#include "history/history.h"
#include "window/themes/window_theme_preview.h"
#include "core/core_settings.h"
#include "core/application.h"
#include "storage/file_download.h"
#include "ui/chat/attach/attach_prepare.h"
#include "ui/image/image.h"
#include <QtCore/QBuffer>
#include <QtGui/QImageReader>
namespace Data {
namespace {
constexpr auto kReadAreaLimit = 12'032 * 9'024;
constexpr auto kWallPaperThumbnailLimit = 960;
constexpr auto kGoodThumbQuality = 87;
enum class FileType {
Video,
VideoSticker,
AnimatedSticker,
WallPaper,
WallPatternPNG,
WallPatternSVG,
Theme,
};
[[nodiscard]] bool MayHaveGoodThumbnail(not_null<DocumentData*> owner) {
return owner->isVideoFile()
|| owner->isAnimation()
|| owner->isWallPaper()
|| owner->isTheme()
|| (owner->sticker() && owner->sticker()->isAnimated());
}
[[nodiscard]] QImage PrepareGoodThumbnail(
const QString &path,
QByteArray data,
FileType type) {
if (type == FileType::Video || type == FileType::VideoSticker) {
auto result = v::get<Ui::PreparedFileInformation::Video>(
::Media::Clip::PrepareForSending(path, data).media);
if (result.isWebmSticker && type == FileType::Video) {
result.thumbnail = Images::Opaque(std::move(result.thumbnail));
}
return result.thumbnail;
} else if (type == FileType::AnimatedSticker) {
return Lottie::ReadThumbnail(Lottie::ReadContent(data, path));
} else if (type == FileType::Theme) {
return Window::Theme::GeneratePreview(data, path);
} else if (type == FileType::WallPatternSVG) {
return Images::Read({
.path = path,
.content = std::move(data),
.maxSize = QSize(
kWallPaperThumbnailLimit,
kWallPaperThumbnailLimit),
.gzipSvg = true,
}).image;
}
auto buffer = QBuffer(&data);
auto file = QFile(path);
auto device = data.isEmpty() ? static_cast<QIODevice*>(&file) : &buffer;
auto reader = QImageReader(device);
const auto size = reader.size();
if (!reader.canRead()
|| (size.width() * size.height() > kReadAreaLimit)) {
return QImage();
}
auto result = reader.read();
if (!result.width() || !result.height()) {
return QImage();
}
return (result.width() > kWallPaperThumbnailLimit
|| result.height() > kWallPaperThumbnailLimit)
? result.scaled(
kWallPaperThumbnailLimit,
kWallPaperThumbnailLimit,
Qt::KeepAspectRatio,
Qt::SmoothTransformation)
: result;
}
} // namespace
VideoPreviewState::VideoPreviewState(DocumentMedia *media)
: _media(media)
, _usingThumbnail(media ? media->owner()->hasVideoThumbnail() : false) {
}
void VideoPreviewState::automaticLoad(Data::FileOrigin origin) const {
Expects(_media != nullptr);
if (_usingThumbnail) {
_media->videoThumbnailWanted(origin);
} else {
_media->automaticLoad(origin, nullptr);
}
}
::Media::Clip::ReaderPointer VideoPreviewState::makeAnimation(
Fn<void(::Media::Clip::Notification)> callback) const {
Expects(_media != nullptr);
Expects(loaded());
return _usingThumbnail
? ::Media::Clip::MakeReader(
_media->videoThumbnailContent(),
std::move(callback))
: ::Media::Clip::MakeReader(
_media->owner()->location(),
_media->bytes(),
std::move(callback));
}
bool VideoPreviewState::usingThumbnail() const {
return _usingThumbnail;
}
bool VideoPreviewState::loading() const {
return _usingThumbnail
? _media->owner()->videoThumbnailLoading()
: _media
? _media->owner()->loading()
: false;
}
bool VideoPreviewState::loaded() const {
return _usingThumbnail
? !_media->videoThumbnailContent().isEmpty()
: _media
? _media->loaded()
: false;
}
DocumentMedia::DocumentMedia(not_null<DocumentData*> owner)
: _owner(owner) {
}
// NB! Right now DocumentMedia can outlive Main::Session!
// In DocumentData::collectLocalData a shared_ptr is sent on_main.
// In case this is a problem the ~Gif code should be rewritten.
DocumentMedia::~DocumentMedia() = default;
not_null<DocumentData*> DocumentMedia::owner() const {
return _owner;
}
void DocumentMedia::goodThumbnailWanted() {
_flags |= Flag::GoodThumbnailWanted;
}
Image *DocumentMedia::goodThumbnail() const {
Expects((_flags & Flag::GoodThumbnailWanted) != 0);
if (!_goodThumbnail) {
ReadOrGenerateThumbnail(_owner);
}
return _goodThumbnail.get();
}
void DocumentMedia::setGoodThumbnail(QImage thumbnail) {
if (!(_flags & Flag::GoodThumbnailWanted)) {
return;
}
_goodThumbnail = std::make_unique<Image>(std::move(thumbnail));
_owner->session().notifyDownloaderTaskFinished();
}
Image *DocumentMedia::thumbnailInline() const {
if (!_inlineThumbnail && !_owner->inlineThumbnailIsPath()) {
const auto bytes = _owner->inlineThumbnailBytes();
if (!bytes.isEmpty()) {
auto image = Images::FromInlineBytes(bytes);
if (image.isNull()) {
_owner->clearInlineThumbnailBytes();
} else {
_inlineThumbnail = std::make_unique<Image>(std::move(image));
}
}
}
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();
}
void DocumentMedia::thumbnailWanted(Data::FileOrigin origin) {
if (!_thumbnail) {
_owner->loadThumbnail(origin);
}
}
QSize DocumentMedia::thumbnailSize() const {
if (const auto image = _thumbnail.get()) {
return image->size();
}
const auto &location = _owner->thumbnailLocation();
return { location.width(), location.height() };
}
void DocumentMedia::setThumbnail(QImage thumbnail) {
_thumbnail = std::make_unique<Image>(std::move(thumbnail));
_owner->session().notifyDownloaderTaskFinished();
}
QByteArray DocumentMedia::videoThumbnailContent() const {
return _videoThumbnailBytes;
}
QSize DocumentMedia::videoThumbnailSize() const {
const auto &location = _owner->videoThumbnailLocation();
return { location.width(), location.height() };
}
void DocumentMedia::videoThumbnailWanted(Data::FileOrigin origin) {
if (_videoThumbnailBytes.isEmpty()) {
_owner->loadVideoThumbnail(origin);
}
}
void DocumentMedia::setVideoThumbnail(QByteArray content) {
_videoThumbnailBytes = std::move(content);
_videoThumbnailBytes.detach();
}
void DocumentMedia::checkStickerLarge() {
if (_sticker) {
return;
}
const auto data = _owner->sticker();
if (!data) {
return;
}
automaticLoad(_owner->stickerSetOrigin(), nullptr);
if (data->isAnimated() || !loaded()) {
return;
}
if (_bytes.isEmpty()) {
const auto &loc = _owner->location(true);
if (loc.accessEnable()) {
_sticker = std::make_unique<Image>(loc.name());
loc.accessDisable();
}
} else {
_sticker = std::make_unique<Image>(_bytes);
}
}
void DocumentMedia::automaticLoad(
Data::FileOrigin origin,
const HistoryItem *item) {
if (_owner->status != FileReady || loaded() || _owner->cancelled()) {
return;
} else if (!item && !_owner->sticker() && !_owner->isAnimation()) {
return;
}
const auto toCache = _owner->saveToCache();
if (!toCache && !Core::App().canSaveFileWithoutAskingForPath()) {
// We need a filename, but we're supposed to ask user for it.
// No automatic download in this case.
return;
}
const auto filename = toCache
? QString()
: DocumentFileNameForSave(_owner);
const auto shouldLoadFromCloud = !Data::IsExecutableName(filename)
&& (item
? Data::AutoDownload::Should(
_owner->session().settings().autoDownload(),
item->history()->peer,
_owner)
: Data::AutoDownload::Should(
_owner->session().settings().autoDownload(),
_owner));
const auto loadFromCloud = shouldLoadFromCloud
? LoadFromCloudOrLocal
: LoadFromLocalOnly;
_owner->save(
origin,
filename,
loadFromCloud,
true);
}
void DocumentMedia::collectLocalData(not_null<DocumentMedia*> local) {
if (const auto image = local->_goodThumbnail.get()) {
_goodThumbnail = std::make_unique<Image>(image->original());
}
if (const auto image = local->_inlineThumbnail.get()) {
_inlineThumbnail = std::make_unique<Image>(image->original());
}
if (const auto image = local->_thumbnail.get()) {
_thumbnail = std::make_unique<Image>(image->original());
}
if (const auto image = local->_sticker.get()) {
_sticker = std::make_unique<Image>(image->original());
}
_bytes = local->_bytes;
_videoThumbnailBytes = local->_videoThumbnailBytes;
_flags = local->_flags;
}
void DocumentMedia::setBytes(const QByteArray &bytes) {
if (!bytes.isEmpty()) {
_bytes = bytes;
}
}
QByteArray DocumentMedia::bytes() const {
return _bytes;
}
bool DocumentMedia::loaded(bool check) const {
return !_bytes.isEmpty() || !_owner->filepath(check).isEmpty();
}
float64 DocumentMedia::progress() const {
return (_owner->uploading() || _owner->loading())
? _owner->progress()
: (loaded() ? 1. : 0.);
}
bool DocumentMedia::canBePlayed(HistoryItem *item) const {
return !_owner->inappPlaybackFailed()
&& _owner->useStreamingLoader()
&& (loaded() || _owner->canBeStreamed(item));
}
bool DocumentMedia::thumbnailEnoughForSticker() const {
const auto &location = owner()->thumbnailLocation();
const auto size = _thumbnail
? QSize(_thumbnail->width(), _thumbnail->height())
: location.valid()
? QSize(location.width(), location.height())
: QSize();
return (size.width() >= 128) || (size.height() >= 128);
}
void DocumentMedia::checkStickerSmall() {
const auto data = _owner->sticker();
if ((data && data->isAnimated()) || thumbnailEnoughForSticker()) {
_owner->loadThumbnail(_owner->stickerSetOrigin());
if (data && data->isAnimated()) {
automaticLoad(_owner->stickerSetOrigin(), nullptr);
}
} else {
checkStickerLarge();
}
}
Image *DocumentMedia::getStickerLarge() {
checkStickerLarge();
return _sticker.get();
}
Image *DocumentMedia::getStickerSmall() {
const auto data = _owner->sticker();
if ((data && data->isAnimated()) || thumbnailEnoughForSticker()) {
return thumbnail();
}
return _sticker.get();
}
void DocumentMedia::checkStickerLarge(not_null<FileLoader*> loader) {
if (_sticker || !_owner->sticker()) {
return;
}
if (auto image = loader->imageData(); !image.isNull()) {
_sticker = std::make_unique<Image>(std::move(image));
}
}
void DocumentMedia::GenerateGoodThumbnail(
not_null<DocumentData*> document,
QByteArray data) {
const auto type = document->isPatternWallPaperSVG()
? FileType::WallPatternSVG
: document->isPatternWallPaperPNG()
? FileType::WallPatternPNG
: document->isWallPaper()
? FileType::WallPaper
: document->isTheme()
? FileType::Theme
: !document->sticker()
? FileType::Video
: document->sticker()->isLottie()
? FileType::AnimatedSticker
: FileType::VideoSticker;
auto location = document->location().isEmpty()
? nullptr
: std::make_unique<Core::FileLocation>(document->location());
if (data.isEmpty() && !location) {
document->setGoodThumbnailChecked(false);
return;
}
const auto guard = base::make_weak(&document->owner().session());
crl::async([=, location = std::move(location)] {
const auto filepath = (location && location->accessEnable())
? location->name()
: QString();
auto result = PrepareGoodThumbnail(filepath, data, type);
auto bytes = QByteArray();
if (!result.isNull()) {
auto buffer = QBuffer(&bytes);
const auto format = (type == FileType::AnimatedSticker
|| type == FileType::VideoSticker)
? "WEBP"
: (type == FileType::WallPatternPNG
|| type == FileType::WallPatternSVG)
? "PNG"
: "JPG";
result.save(&buffer, format, kGoodThumbQuality);
}
if (!filepath.isEmpty()) {
location->accessDisable();
}
const auto cache = bytes.isEmpty() ? QByteArray("(failed)") : bytes;
crl::on_main(guard, [=] {
document->setGoodThumbnailChecked(true);
if (const auto active = document->activeMediaView()) {
active->setGoodThumbnail(result);
}
document->owner().cache().put(
document->goodThumbnailCacheKey(),
Storage::Cache::Database::TaggedValue{
base::duplicate(cache),
kImageCacheTag });
});
});
}
void DocumentMedia::CheckGoodThumbnail(not_null<DocumentData*> document) {
if (!document->goodThumbnailChecked()) {
ReadOrGenerateThumbnail(document);
}
}
void DocumentMedia::ReadOrGenerateThumbnail(
not_null<DocumentData*> document) {
if (document->goodThumbnailGenerating()
|| document->goodThumbnailNoData()
|| !MayHaveGoodThumbnail(document)) {
return;
}
document->setGoodThumbnailGenerating();
const auto guard = base::make_weak(&document->session());
const auto active = document->activeMediaView();
const auto got = [=](QByteArray value) {
if (value.isEmpty()) {
const auto bytes = active ? active->bytes() : QByteArray();
crl::on_main(guard, [=] {
GenerateGoodThumbnail(document, bytes);
});
} else if (active) {
crl::async([=] {
auto image = Images::Read({ .content = value }).image;
crl::on_main(guard, [=, image = std::move(image)]() mutable {
document->setGoodThumbnailChecked(true);
if (const auto active = document->activeMediaView()) {
active->setGoodThumbnail(std::move(image));
}
});
});
} else {
crl::on_main(guard, [=] {
document->setGoodThumbnailChecked(true);
});
}
};
document->owner().cache().get(document->goodThumbnailCacheKey(), got);
}
auto DocumentIconFrameGenerator(not_null<DocumentMedia*> media)
-> FnMut<std::unique_ptr<Ui::FrameGenerator>()> {
if (!media->loaded()) {
return nullptr;
}
using Type = StickerType;
const auto document = media->owner();
const auto content = media->bytes();
const auto fromFile = content.isEmpty();
const auto type = document->sticker()
? document->sticker()->type
: (document->isVideoFile() || document->isAnimation())
? Type::Webm
: Type::Webp;
const auto &location = media->owner()->location(true);
if (fromFile && !location.accessEnable()) {
return nullptr;
}
return [=]() -> std::unique_ptr<Ui::FrameGenerator> {
const auto bytes = Lottie::ReadContent(content, location.name());
if (fromFile) {
location.accessDisable();
}
if (bytes.isEmpty()) {
return nullptr;
}
switch (type) {
case Type::Tgs:
return std::make_unique<Lottie::FrameGenerator>(bytes);
case Type::Webm:
return std::make_unique<FFmpeg::FrameGenerator>(bytes);
case Type::Webp:
return std::make_unique<Ui::ImageFrameGenerator>(bytes);
}
Unexpected("Document type in DocumentIconFrameGenerator.");
};
}
auto DocumentIconFrameGenerator(const std::shared_ptr<DocumentMedia> &media)
-> FnMut<std::unique_ptr<Ui::FrameGenerator>()> {
return DocumentIconFrameGenerator(media.get());
}
} // namespace Data