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