1 /*
2 This file is part of Telegram Desktop,
3 the official desktop application for the Telegram messaging service.
4
5 For license and copyright information please follow this link:
6 https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
7 */
8 #include "data/data_document_media.h"
9
10 #include "data/data_document.h"
11 #include "data/data_document_resolver.h"
12 #include "data/data_session.h"
13 #include "data/data_cloud_themes.h"
14 #include "data/data_file_origin.h"
15 #include "data/data_auto_download.h"
16 #include "media/clip/media_clip_reader.h"
17 #include "main/main_session.h"
18 #include "main/main_session_settings.h"
19 #include "lottie/lottie_animation.h"
20 #include "history/history_item.h"
21 #include "history/history.h"
22 #include "window/themes/window_theme_preview.h"
23 #include "core/core_settings.h"
24 #include "core/application.h"
25 #include "storage/file_download.h"
26 #include "ui/image/image.h"
27
28 #include <QtCore/QBuffer>
29 #include <QtGui/QImageReader>
30
31 namespace Data {
32 namespace {
33
34 constexpr auto kReadAreaLimit = 12'032 * 9'024;
35 constexpr auto kWallPaperThumbnailLimit = 960;
36 constexpr auto kGoodThumbQuality = 87;
37
38 enum class FileType {
39 Video,
40 AnimatedSticker,
41 WallPaper,
42 WallPatternPNG,
43 WallPatternSVG,
44 Theme,
45 };
46
MayHaveGoodThumbnail(not_null<DocumentData * > owner)47 [[nodiscard]] bool MayHaveGoodThumbnail(not_null<DocumentData*> owner) {
48 return owner->isVideoFile()
49 || owner->isAnimation()
50 || owner->isWallPaper()
51 || owner->isTheme()
52 || (owner->sticker() && owner->sticker()->animated);
53 }
54
PrepareGoodThumbnail(const QString & path,QByteArray data,FileType type)55 [[nodiscard]] QImage PrepareGoodThumbnail(
56 const QString &path,
57 QByteArray data,
58 FileType type) {
59 if (type == FileType::Video) {
60 return ::Media::Clip::PrepareForSending(path, data).thumbnail;
61 } else if (type == FileType::AnimatedSticker) {
62 return Lottie::ReadThumbnail(Lottie::ReadContent(data, path));
63 } else if (type == FileType::Theme) {
64 return Window::Theme::GeneratePreview(data, path);
65 } else if (type == FileType::WallPatternSVG) {
66 return Images::Read({
67 .path = path,
68 .content = std::move(data),
69 .maxSize = QSize(
70 kWallPaperThumbnailLimit,
71 kWallPaperThumbnailLimit),
72 .gzipSvg = true,
73 }).image;
74 }
75 auto buffer = QBuffer(&data);
76 auto file = QFile(path);
77 auto device = data.isEmpty() ? static_cast<QIODevice*>(&file) : &buffer;
78 auto reader = QImageReader(device);
79 const auto size = reader.size();
80 if (!reader.canRead()
81 || (size.width() * size.height() > kReadAreaLimit)) {
82 return QImage();
83 }
84 auto result = reader.read();
85 if (!result.width() || !result.height()) {
86 return QImage();
87 }
88 return (result.width() > kWallPaperThumbnailLimit
89 || result.height() > kWallPaperThumbnailLimit)
90 ? result.scaled(
91 kWallPaperThumbnailLimit,
92 kWallPaperThumbnailLimit,
93 Qt::KeepAspectRatio,
94 Qt::SmoothTransformation)
95 : result;
96 }
97
98 } // namespace
99
VideoPreviewState(DocumentMedia * media)100 VideoPreviewState::VideoPreviewState(DocumentMedia *media)
101 : _media(media)
102 , _usingThumbnail(media ? media->owner()->hasVideoThumbnail() : false) {
103 }
104
automaticLoad(Data::FileOrigin origin) const105 void VideoPreviewState::automaticLoad(Data::FileOrigin origin) const {
106 Expects(_media != nullptr);
107
108 if (_usingThumbnail) {
109 _media->videoThumbnailWanted(origin);
110 } else {
111 _media->automaticLoad(origin, nullptr);
112 }
113 }
114
makeAnimation(Fn<void (::Media::Clip::Notification)> callback) const115 ::Media::Clip::ReaderPointer VideoPreviewState::makeAnimation(
116 Fn<void(::Media::Clip::Notification)> callback) const {
117 Expects(_media != nullptr);
118 Expects(loaded());
119
120 return _usingThumbnail
121 ? ::Media::Clip::MakeReader(
122 _media->videoThumbnailContent(),
123 std::move(callback))
124 : ::Media::Clip::MakeReader(
125 _media->owner()->location(),
126 _media->bytes(),
127 std::move(callback));
128 }
129
usingThumbnail() const130 bool VideoPreviewState::usingThumbnail() const {
131 return _usingThumbnail;
132 }
133
loading() const134 bool VideoPreviewState::loading() const {
135 return _usingThumbnail
136 ? _media->owner()->videoThumbnailLoading()
137 : _media
138 ? _media->owner()->loading()
139 : false;
140 }
141
loaded() const142 bool VideoPreviewState::loaded() const {
143 return _usingThumbnail
144 ? !_media->videoThumbnailContent().isEmpty()
145 : _media
146 ? _media->loaded()
147 : false;
148 }
149
DocumentMedia(not_null<DocumentData * > owner)150 DocumentMedia::DocumentMedia(not_null<DocumentData*> owner)
151 : _owner(owner) {
152 }
153
154 // NB! Right now DocumentMedia can outlive Main::Session!
155 // In DocumentData::collectLocalData a shared_ptr is sent on_main.
156 // In case this is a problem the ~Gif code should be rewritten.
157 DocumentMedia::~DocumentMedia() = default;
158
owner() const159 not_null<DocumentData*> DocumentMedia::owner() const {
160 return _owner;
161 }
162
goodThumbnailWanted()163 void DocumentMedia::goodThumbnailWanted() {
164 _flags |= Flag::GoodThumbnailWanted;
165 }
166
goodThumbnail() const167 Image *DocumentMedia::goodThumbnail() const {
168 Expects((_flags & Flag::GoodThumbnailWanted) != 0);
169
170 if (!_goodThumbnail) {
171 ReadOrGenerateThumbnail(_owner);
172 }
173 return _goodThumbnail.get();
174 }
175
setGoodThumbnail(QImage thumbnail)176 void DocumentMedia::setGoodThumbnail(QImage thumbnail) {
177 if (!(_flags & Flag::GoodThumbnailWanted)) {
178 return;
179 }
180 _goodThumbnail = std::make_unique<Image>(std::move(thumbnail));
181 _owner->session().notifyDownloaderTaskFinished();
182 }
183
thumbnailInline() const184 Image *DocumentMedia::thumbnailInline() const {
185 if (!_inlineThumbnail && !_owner->inlineThumbnailIsPath()) {
186 const auto bytes = _owner->inlineThumbnailBytes();
187 if (!bytes.isEmpty()) {
188 auto image = Images::FromInlineBytes(bytes);
189 if (image.isNull()) {
190 _owner->clearInlineThumbnailBytes();
191 } else {
192 _inlineThumbnail = std::make_unique<Image>(std::move(image));
193 }
194 }
195 }
196 return _inlineThumbnail.get();
197 }
198
thumbnailPath() const199 const QPainterPath &DocumentMedia::thumbnailPath() const {
200 if (_pathThumbnail.isEmpty() && _owner->inlineThumbnailIsPath()) {
201 const auto bytes = _owner->inlineThumbnailBytes();
202 if (!bytes.isEmpty()) {
203 _pathThumbnail = Images::PathFromInlineBytes(bytes);
204 if (_pathThumbnail.isEmpty()) {
205 _owner->clearInlineThumbnailBytes();
206 }
207 }
208 }
209 return _pathThumbnail;
210 }
211
thumbnail() const212 Image *DocumentMedia::thumbnail() const {
213 return _thumbnail.get();
214 }
215
thumbnailWanted(Data::FileOrigin origin)216 void DocumentMedia::thumbnailWanted(Data::FileOrigin origin) {
217 if (!_thumbnail) {
218 _owner->loadThumbnail(origin);
219 }
220 }
221
thumbnailSize() const222 QSize DocumentMedia::thumbnailSize() const {
223 if (const auto image = _thumbnail.get()) {
224 return image->size();
225 }
226 const auto &location = _owner->thumbnailLocation();
227 return { location.width(), location.height() };
228 }
229
setThumbnail(QImage thumbnail)230 void DocumentMedia::setThumbnail(QImage thumbnail) {
231 _thumbnail = std::make_unique<Image>(std::move(thumbnail));
232 _owner->session().notifyDownloaderTaskFinished();
233 }
234
videoThumbnailContent() const235 QByteArray DocumentMedia::videoThumbnailContent() const {
236 return _videoThumbnailBytes;
237 }
238
videoThumbnailSize() const239 QSize DocumentMedia::videoThumbnailSize() const {
240 const auto &location = _owner->videoThumbnailLocation();
241 return { location.width(), location.height() };
242 }
243
videoThumbnailWanted(Data::FileOrigin origin)244 void DocumentMedia::videoThumbnailWanted(Data::FileOrigin origin) {
245 if (_videoThumbnailBytes.isEmpty()) {
246 _owner->loadVideoThumbnail(origin);
247 }
248 }
249
setVideoThumbnail(QByteArray content)250 void DocumentMedia::setVideoThumbnail(QByteArray content) {
251 _videoThumbnailBytes = std::move(content);
252 }
253
checkStickerLarge()254 void DocumentMedia::checkStickerLarge() {
255 if (_sticker) {
256 return;
257 }
258 const auto data = _owner->sticker();
259 if (!data) {
260 return;
261 }
262 automaticLoad(_owner->stickerSetOrigin(), nullptr);
263 if (data->animated || !loaded()) {
264 return;
265 }
266 if (_bytes.isEmpty()) {
267 const auto &loc = _owner->location(true);
268 if (loc.accessEnable()) {
269 _sticker = std::make_unique<Image>(loc.name());
270 loc.accessDisable();
271 }
272 } else {
273 _sticker = std::make_unique<Image>(_bytes);
274 }
275 }
276
automaticLoad(Data::FileOrigin origin,const HistoryItem * item)277 void DocumentMedia::automaticLoad(
278 Data::FileOrigin origin,
279 const HistoryItem *item) {
280 if (_owner->status != FileReady || loaded() || _owner->cancelled()) {
281 return;
282 } else if (!item && !_owner->sticker() && !_owner->isAnimation()) {
283 return;
284 }
285 const auto toCache = _owner->saveToCache();
286 if (!toCache && Core::App().settings().askDownloadPath()) {
287 // We need a filename, but we're supposed to ask user for it.
288 // No automatic download in this case.
289 return;
290 }
291 const auto filename = toCache
292 ? QString()
293 : DocumentFileNameForSave(_owner);
294 const auto shouldLoadFromCloud = !Data::IsExecutableName(filename)
295 && (item
296 ? Data::AutoDownload::Should(
297 _owner->session().settings().autoDownload(),
298 item->history()->peer,
299 _owner)
300 : Data::AutoDownload::Should(
301 _owner->session().settings().autoDownload(),
302 _owner));
303 const auto loadFromCloud = shouldLoadFromCloud
304 ? LoadFromCloudOrLocal
305 : LoadFromLocalOnly;
306 _owner->save(
307 origin,
308 filename,
309 loadFromCloud,
310 true);
311 }
312
collectLocalData(not_null<DocumentMedia * > local)313 void DocumentMedia::collectLocalData(not_null<DocumentMedia*> local) {
314 if (const auto image = local->_goodThumbnail.get()) {
315 _goodThumbnail = std::make_unique<Image>(image->original());
316 }
317 if (const auto image = local->_inlineThumbnail.get()) {
318 _inlineThumbnail = std::make_unique<Image>(image->original());
319 }
320 if (const auto image = local->_thumbnail.get()) {
321 _thumbnail = std::make_unique<Image>(image->original());
322 }
323 if (const auto image = local->_sticker.get()) {
324 _sticker = std::make_unique<Image>(image->original());
325 }
326 _bytes = local->_bytes;
327 _videoThumbnailBytes = local->_videoThumbnailBytes;
328 _flags = local->_flags;
329 }
330
setBytes(const QByteArray & bytes)331 void DocumentMedia::setBytes(const QByteArray &bytes) {
332 if (!bytes.isEmpty()) {
333 _bytes = bytes;
334 }
335 }
336
bytes() const337 QByteArray DocumentMedia::bytes() const {
338 return _bytes;
339 }
340
loaded(bool check) const341 bool DocumentMedia::loaded(bool check) const {
342 return !_bytes.isEmpty() || !_owner->filepath(check).isEmpty();
343 }
344
progress() const345 float64 DocumentMedia::progress() const {
346 return (_owner->uploading() || _owner->loading())
347 ? _owner->progress()
348 : (loaded() ? 1. : 0.);
349 }
350
canBePlayed() const351 bool DocumentMedia::canBePlayed() const {
352 return !_owner->inappPlaybackFailed()
353 && _owner->useStreamingLoader()
354 && (loaded() || _owner->canBeStreamed());
355 }
356
thumbnailEnoughForSticker() const357 bool DocumentMedia::thumbnailEnoughForSticker() const {
358 const auto &location = owner()->thumbnailLocation();
359 const auto size = _thumbnail
360 ? QSize(_thumbnail->width(), _thumbnail->height())
361 : location.valid()
362 ? QSize(location.width(), location.height())
363 : QSize();
364 return (size.width() >= 128) || (size.height() >= 128);
365 }
366
checkStickerSmall()367 void DocumentMedia::checkStickerSmall() {
368 const auto data = _owner->sticker();
369 if ((data && data->animated) || thumbnailEnoughForSticker()) {
370 _owner->loadThumbnail(_owner->stickerSetOrigin());
371 if (data && data->animated) {
372 automaticLoad(_owner->stickerSetOrigin(), nullptr);
373 }
374 } else {
375 checkStickerLarge();
376 }
377 }
378
getStickerLarge()379 Image *DocumentMedia::getStickerLarge() {
380 checkStickerLarge();
381 return _sticker.get();
382 }
383
getStickerSmall()384 Image *DocumentMedia::getStickerSmall() {
385 const auto data = _owner->sticker();
386 if ((data && data->animated) || thumbnailEnoughForSticker()) {
387 return thumbnail();
388 }
389 return _sticker.get();
390 }
391
checkStickerLarge(not_null<FileLoader * > loader)392 void DocumentMedia::checkStickerLarge(not_null<FileLoader*> loader) {
393 if (_sticker || !_owner->sticker()) {
394 return;
395 }
396 if (auto image = loader->imageData(); !image.isNull()) {
397 _sticker = std::make_unique<Image>(std::move(image));
398 }
399 }
400
GenerateGoodThumbnail(not_null<DocumentData * > document,QByteArray data)401 void DocumentMedia::GenerateGoodThumbnail(
402 not_null<DocumentData*> document,
403 QByteArray data) {
404 const auto type = document->isPatternWallPaperSVG()
405 ? FileType::WallPatternSVG
406 : document->isPatternWallPaperPNG()
407 ? FileType::WallPatternPNG
408 : document->isWallPaper()
409 ? FileType::WallPaper
410 : document->isTheme()
411 ? FileType::Theme
412 : document->sticker()
413 ? FileType::AnimatedSticker
414 : FileType::Video;
415 auto location = document->location().isEmpty()
416 ? nullptr
417 : std::make_unique<Core::FileLocation>(document->location());
418 if (data.isEmpty() && !location) {
419 document->setGoodThumbnailChecked(false);
420 return;
421 }
422 const auto guard = base::make_weak(&document->owner().session());
423 crl::async([=, location = std::move(location)] {
424 const auto filepath = (location && location->accessEnable())
425 ? location->name()
426 : QString();
427 auto result = PrepareGoodThumbnail(filepath, data, type);
428 auto bytes = QByteArray();
429 if (!result.isNull()) {
430 auto buffer = QBuffer(&bytes);
431 const auto format = (type == FileType::AnimatedSticker)
432 ? "WEBP"
433 : (type == FileType::WallPatternPNG
434 || type == FileType::WallPatternSVG)
435 ? "PNG"
436 : "JPG";
437 result.save(&buffer, format, kGoodThumbQuality);
438 }
439 if (!filepath.isEmpty()) {
440 location->accessDisable();
441 }
442 const auto cache = bytes.isEmpty() ? QByteArray("(failed)") : bytes;
443 crl::on_main(guard, [=] {
444 document->setGoodThumbnailChecked(true);
445 if (const auto active = document->activeMediaView()) {
446 active->setGoodThumbnail(result);
447 }
448 document->owner().cache().put(
449 document->goodThumbnailCacheKey(),
450 Storage::Cache::Database::TaggedValue{
451 base::duplicate(cache),
452 kImageCacheTag });
453 });
454 });
455 }
456
CheckGoodThumbnail(not_null<DocumentData * > document)457 void DocumentMedia::CheckGoodThumbnail(not_null<DocumentData*> document) {
458 if (!document->goodThumbnailChecked()) {
459 ReadOrGenerateThumbnail(document);
460 }
461 }
462
ReadOrGenerateThumbnail(not_null<DocumentData * > document)463 void DocumentMedia::ReadOrGenerateThumbnail(
464 not_null<DocumentData*> document) {
465 if (document->goodThumbnailGenerating()
466 || document->goodThumbnailNoData()
467 || !MayHaveGoodThumbnail(document)) {
468 return;
469 }
470 document->setGoodThumbnailGenerating();
471
472 const auto guard = base::make_weak(&document->session());
473 const auto active = document->activeMediaView();
474 const auto got = [=](QByteArray value) {
475 if (value.isEmpty()) {
476 const auto bytes = active ? active->bytes() : QByteArray();
477 crl::on_main(guard, [=] {
478 GenerateGoodThumbnail(document, bytes);
479 });
480 } else if (active) {
481 crl::async([=] {
482 auto image = Images::Read({ .content = value }).image;
483 crl::on_main(guard, [=, image = std::move(image)]() mutable {
484 document->setGoodThumbnailChecked(true);
485 if (const auto active = document->activeMediaView()) {
486 active->setGoodThumbnail(std::move(image));
487 }
488 });
489 });
490 } else {
491 crl::on_main(guard, [=] {
492 document->setGoodThumbnailChecked(true);
493 });
494 }
495 };
496 document->owner().cache().get(document->goodThumbnailCacheKey(), got);
497 }
498
499 } // namespace Data
500