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 "window/window_media_preview.h"
9 
10 #include "data/data_photo.h"
11 #include "data/data_photo_media.h"
12 #include "data/data_document.h"
13 #include "data/data_document_media.h"
14 #include "data/data_session.h"
15 #include "data/stickers/data_stickers.h"
16 #include "ui/image/image.h"
17 #include "ui/emoji_config.h"
18 #include "lottie/lottie_single_player.h"
19 #include "main/main_session.h"
20 #include "window/window_session_controller.h"
21 #include "styles/style_layers.h"
22 #include "styles/style_chat_helpers.h"
23 #include "styles/style_chat.h"
24 
25 namespace Window {
26 namespace {
27 
28 constexpr int kStickerPreviewEmojiLimit = 10;
29 
30 } // namespace
31 
MediaPreviewWidget(QWidget * parent,not_null<Window::SessionController * > controller)32 MediaPreviewWidget::MediaPreviewWidget(
33 	QWidget *parent,
34 	not_null<Window::SessionController*> controller)
35 : RpWidget(parent)
36 , _controller(controller)
37 , _emojiSize(Ui::Emoji::GetSizeLarge() / cIntRetinaFactor()) {
38 	setAttribute(Qt::WA_TransparentForMouseEvents);
39 	_controller->session().downloaderTaskFinished(
40 	) | rpl::start_with_next([=] {
41 		update();
42 	}, lifetime());
43 }
44 
updateArea() const45 QRect MediaPreviewWidget::updateArea() const {
46 	const auto size = currentDimensions();
47 	return QRect(
48 		QPoint((width() - size.width()) / 2, (height() - size.height()) / 2),
49 		size);
50 }
51 
paintEvent(QPaintEvent * e)52 void MediaPreviewWidget::paintEvent(QPaintEvent *e) {
53 	Painter p(this);
54 	QRect r(e->rect());
55 
56 	const auto image = [&] {
57 		if (!_lottie || !_lottie->ready()) {
58 			return QImage();
59 		}
60 		_lottie->markFrameShown();
61 		return _lottie->frame();
62 	}();
63 	const auto pixmap = image.isNull() ? currentImage() : QPixmap();
64 	const auto size = image.isNull() ? pixmap.size() : image.size();
65 	int w = size.width() / cIntRetinaFactor(), h = size.height() / cIntRetinaFactor();
66 	auto shown = _a_shown.value(_hiding ? 0. : 1.);
67 	if (!_a_shown.animating()) {
68 		if (_hiding) {
69 			hide();
70 			_controller->disableGifPauseReason(Window::GifPauseReason::MediaPreview);
71 			return;
72 		}
73 	} else {
74 		p.setOpacity(shown);
75 //		w = qMax(qRound(w * (st::stickerPreviewMin + ((1. - st::stickerPreviewMin) * shown)) / 2.) * 2 + int(w % 2), 1);
76 //		h = qMax(qRound(h * (st::stickerPreviewMin + ((1. - st::stickerPreviewMin) * shown)) / 2.) * 2 + int(h % 2), 1);
77 	}
78 	p.fillRect(r, st::stickerPreviewBg);
79 	if (image.isNull()) {
80 		p.drawPixmap((width() - w) / 2, (height() - h) / 2, pixmap);
81 	} else {
82 		p.drawImage(
83 			QRect((width() - w) / 2, (height() - h) / 2, w, h),
84 			image);
85 	}
86 	if (!_emojiList.empty()) {
87 		const auto emojiCount = _emojiList.size();
88 		const auto emojiWidth = (emojiCount * _emojiSize) + (emojiCount - 1) * st::stickerEmojiSkip;
89 		auto emojiLeft = (width() - emojiWidth) / 2;
90 		const auto esize = Ui::Emoji::GetSizeLarge();
91 		for (const auto emoji : _emojiList) {
92 			Ui::Emoji::Draw(
93 				p,
94 				emoji,
95 				esize,
96 				emojiLeft,
97 				(height() - h) / 2 - (_emojiSize * 2));
98 			emojiLeft += _emojiSize + st::stickerEmojiSkip;
99 		}
100 	}
101 }
102 
resizeEvent(QResizeEvent * e)103 void MediaPreviewWidget::resizeEvent(QResizeEvent *e) {
104 	update();
105 }
106 
showPreview(Data::FileOrigin origin,not_null<DocumentData * > document)107 void MediaPreviewWidget::showPreview(
108 		Data::FileOrigin origin,
109 		not_null<DocumentData*> document) {
110 	if (!document
111 		|| (!document->isAnimation() && !document->sticker())
112 		|| document->isVideoMessage()) {
113 		hidePreview();
114 		return;
115 	}
116 
117 	startShow();
118 	_origin = origin;
119 	_photo = nullptr;
120 	_photoMedia = nullptr;
121 	_document = document;
122 	_documentMedia = _document->createMediaView();
123 	_documentMedia->thumbnailWanted(_origin);
124 	_documentMedia->videoThumbnailWanted(_origin);
125 	_documentMedia->automaticLoad(_origin, nullptr);
126 	fillEmojiString();
127 	resetGifAndCache();
128 }
129 
showPreview(Data::FileOrigin origin,not_null<PhotoData * > photo)130 void MediaPreviewWidget::showPreview(
131 		Data::FileOrigin origin,
132 		not_null<PhotoData*> photo) {
133 	startShow();
134 	_origin = origin;
135 	_document = nullptr;
136 	_documentMedia = nullptr;
137 	_photo = photo;
138 	_photoMedia = _photo->createMediaView();
139 	fillEmojiString();
140 	resetGifAndCache();
141 }
142 
startShow()143 void MediaPreviewWidget::startShow() {
144 	_cache = QPixmap();
145 	if (isHidden() || _a_shown.animating()) {
146 		if (isHidden()) {
147 			show();
148 			_controller->enableGifPauseReason(Window::GifPauseReason::MediaPreview);
149 		}
150 		_hiding = false;
151 		_a_shown.start([=] { update(); }, 0., 1., st::stickerPreviewDuration);
152 	} else {
153 		update();
154 	}
155 }
156 
hidePreview()157 void MediaPreviewWidget::hidePreview() {
158 	if (isHidden()) {
159 		return;
160 	}
161 	if (_gif || _gifThumbnail) {
162 		_cache = currentImage();
163 	}
164 	_hiding = true;
165 	_a_shown.start([=] { update(); }, 1., 0., st::stickerPreviewDuration);
166 	_photo = nullptr;
167 	_photoMedia = nullptr;
168 	_document = nullptr;
169 	_documentMedia = nullptr;
170 	resetGifAndCache();
171 }
172 
fillEmojiString()173 void MediaPreviewWidget::fillEmojiString() {
174 	_emojiList.clear();
175 	if (_photo) {
176 		return;
177 	}
178 	if (auto sticker = _document->sticker()) {
179 		if (auto list = _document->owner().stickers().getEmojiListFromSet(_document)) {
180 			_emojiList = std::move(*list);
181 			while (_emojiList.size() > kStickerPreviewEmojiLimit) {
182 				_emojiList.pop_back();
183 			}
184 		} else if (const auto emoji = Ui::Emoji::Find(sticker->alt)) {
185 			_emojiList.emplace_back(emoji);
186 		}
187 	}
188 }
189 
resetGifAndCache()190 void MediaPreviewWidget::resetGifAndCache() {
191 	_lottie = nullptr;
192 	_gif.reset();
193 	_gifThumbnail.reset();
194 	_gifLastPosition = 0;
195 	_cacheStatus = CacheNotLoaded;
196 	_cachedSize = QSize();
197 }
198 
currentDimensions() const199 QSize MediaPreviewWidget::currentDimensions() const {
200 	if (!_cachedSize.isEmpty()) {
201 		return _cachedSize;
202 	}
203 	if (!_document && !_photo) {
204 		_cachedSize = QSize(_cache.width() / cIntRetinaFactor(), _cache.height() / cIntRetinaFactor());
205 		return _cachedSize;
206 	}
207 
208 	QSize result, box;
209 	if (_photo) {
210 		result = QSize(_photo->width(), _photo->height());
211 		const auto skip = st::defaultBox.margin.top();
212 		box = QSize(width() - 2 * skip, height() - 2 * skip);
213 	} else {
214 		result = _document->dimensions;
215 		if (result.isEmpty()) {
216 			const auto &gif = (_gif && _gif->ready()) ? _gif : _gifThumbnail;
217 			if (gif && gif->ready()) {
218 				result = QSize(gif->width(), gif->height());
219 			}
220 		}
221 		if (_document->sticker()) {
222 			box = QSize(st::maxStickerSize, st::maxStickerSize);
223 		} else {
224 			box = QSize(2 * st::maxStickerSize, 2 * st::maxStickerSize);
225 		}
226 	}
227 	result = QSize(qMax(style::ConvertScale(result.width()), 1), qMax(style::ConvertScale(result.height()), 1));
228 	if (result.width() > box.width()) {
229 		result.setHeight(qMax((box.width() * result.height()) / result.width(), 1));
230 		result.setWidth(box.width());
231 	}
232 	if (result.height() > box.height()) {
233 		result.setWidth(qMax((box.height() * result.width()) / result.height(), 1));
234 		result.setHeight(box.height());
235 	}
236 	if (_photo) {
237 		_cachedSize = result;
238 	}
239 	return result;
240 }
241 
setupLottie()242 void MediaPreviewWidget::setupLottie() {
243 	Expects(_document != nullptr);
244 
245 	_lottie = std::make_unique<Lottie::SinglePlayer>(
246 		Lottie::ReadContent(_documentMedia->bytes(), _document->filepath()),
247 		Lottie::FrameRequest{ currentDimensions() * cIntRetinaFactor() },
248 		Lottie::Quality::High);
249 
250 	_lottie->updates(
251 	) | rpl::start_with_next([=](Lottie::Update update) {
252 		v::match(update.data, [&](const Lottie::Information &) {
253 			this->update();
254 		}, [&](const Lottie::DisplayFrameRequest &) {
255 			this->update(updateArea());
256 		});
257 	}, lifetime());
258 }
259 
currentImage() const260 QPixmap MediaPreviewWidget::currentImage() const {
261 	if (_document) {
262 		if (const auto sticker = _document->sticker()) {
263 			if (_cacheStatus != CacheLoaded) {
264 				if (sticker->animated && !_lottie && _documentMedia->loaded()) {
265 					const_cast<MediaPreviewWidget*>(this)->setupLottie();
266 				}
267 				if (_lottie && _lottie->ready()) {
268 					return QPixmap();
269 				} else if (const auto image = _documentMedia->getStickerLarge()) {
270 					QSize s = currentDimensions();
271 					_cache = image->pix(s.width(), s.height());
272 					_cacheStatus = CacheLoaded;
273 				} else if (_cacheStatus != CacheThumbLoaded
274 					&& _document->hasThumbnail()
275 					&& _documentMedia->thumbnail()) {
276 					QSize s = currentDimensions();
277 					_cache = _documentMedia->thumbnail()->pixBlurred(s.width(), s.height());
278 					_cacheStatus = CacheThumbLoaded;
279 				}
280 			}
281 		} else {
282 			const_cast<MediaPreviewWidget*>(this)->validateGifAnimation();
283 			const auto &gif = (_gif && _gif->started())
284 				? _gif
285 				: _gifThumbnail;
286 			if (gif && gif->started()) {
287 				auto s = currentDimensions();
288 				auto paused = _controller->isGifPausedAtLeastFor(Window::GifPauseReason::MediaPreview);
289 				return gif->current(s.width(), s.height(), s.width(), s.height(), ImageRoundRadius::None, RectPart::None, paused ? 0 : crl::now());
290 			}
291 			if (_cacheStatus != CacheThumbLoaded
292 				&& _document->hasThumbnail()) {
293 				QSize s = currentDimensions();
294 				const auto thumbnail = _documentMedia->thumbnail();
295 				if (thumbnail) {
296 					_cache = thumbnail->pixBlurred(s.width(), s.height());
297 					_cacheStatus = CacheThumbLoaded;
298 				} else if (const auto blurred = _documentMedia->thumbnailInline()) {
299 					_cache = blurred->pixBlurred(s.width(), s.height());
300 					_cacheStatus = CacheThumbLoaded;
301 				}
302 			}
303 		}
304 	} else if (_photo) {
305 		if (_cacheStatus != CacheLoaded) {
306 			if (_photoMedia->loaded()) {
307 				QSize s = currentDimensions();
308 				_cache = _photoMedia->image(Data::PhotoSize::Large)->pix(s.width(), s.height());
309 				_cacheStatus = CacheLoaded;
310 			} else {
311 				_photo->load(_origin);
312 				if (_cacheStatus != CacheThumbLoaded) {
313 					QSize s = currentDimensions();
314 					if (const auto thumbnail = _photoMedia->image(
315 							Data::PhotoSize::Thumbnail)) {
316 						_cache = thumbnail->pixBlurred(s.width(), s.height());
317 						_cacheStatus = CacheThumbLoaded;
318 					} else if (const auto small = _photoMedia->image(
319 							Data::PhotoSize::Small)) {
320 						_cache = small->pixBlurred(s.width(), s.height());
321 						_cacheStatus = CacheThumbLoaded;
322 					} else if (const auto blurred = _photoMedia->thumbnailInline()) {
323 						_cache = blurred->pixBlurred(s.width(), s.height());
324 						_cacheStatus = CacheThumbLoaded;
325 					} else {
326 						_photoMedia->wanted(Data::PhotoSize::Small, _origin);
327 					}
328 				}
329 			}
330 		}
331 	}
332 	return _cache;
333 }
334 
startGifAnimation(const Media::Clip::ReaderPointer & gif)335 void MediaPreviewWidget::startGifAnimation(
336 		const Media::Clip::ReaderPointer &gif) {
337 	const auto s = currentDimensions();
338 	gif->start(
339 		s.width(),
340 		s.height(),
341 		s.width(),
342 		s.height(),
343 		ImageRoundRadius::None,
344 		RectPart::None);
345 }
346 
validateGifAnimation()347 void MediaPreviewWidget::validateGifAnimation() {
348 	Expects(_documentMedia != nullptr);
349 
350 	if (_gifThumbnail && _gifThumbnail->started()) {
351 		const auto position = _gifThumbnail->getPositionMs();
352 		if (_gif
353 			&& _gif->ready()
354 			&& !_gif->started()
355 			&& (_gifLastPosition > position)) {
356 			startGifAnimation(_gif);
357 			_gifThumbnail.reset();
358 			_gifLastPosition = 0;
359 			return;
360 		} else {
361 			_gifLastPosition = position;
362 		}
363 	} else if (_gif || _gif.isBad()) {
364 		return;
365 	}
366 
367 	const auto contentLoaded = _documentMedia->loaded();
368 	const auto thumbContent = _documentMedia->videoThumbnailContent();
369 	const auto thumbLoaded = !thumbContent.isEmpty();
370 	if (!contentLoaded
371 		&& (_gifThumbnail || _gifThumbnail.isBad() | !thumbLoaded)) {
372 		return;
373 	}
374 	const auto callback = [=](Media::Clip::Notification notification) {
375 		clipCallback(notification);
376 	};
377 	if (contentLoaded) {
378 		_gif = Media::Clip::MakeReader(
379 			_documentMedia->owner()->location(),
380 			_documentMedia->bytes(),
381 			std::move(callback));
382 	} else {
383 		_gifThumbnail = Media::Clip::MakeReader(
384 			thumbContent,
385 			std::move(callback));
386 	}
387 }
388 
clipCallback(Media::Clip::Notification notification)389 void MediaPreviewWidget::clipCallback(
390 		Media::Clip::Notification notification) {
391 	using namespace Media::Clip;
392 	switch (notification) {
393 	case NotificationReinit: {
394 		if (_gifThumbnail && _gifThumbnail->state() == State::Error) {
395 			_gifThumbnail.setBad();
396 		}
397 		if (_gif && _gif->state() == State::Error) {
398 			_gif.setBad();
399 		}
400 
401 		if (_gif
402 			&& _gif->ready()
403 			&& !_gif->started()
404 			&& (!_gifThumbnail || !_gifThumbnail->started())) {
405 			startGifAnimation(_gif);
406 		} else if (!_gif
407 			&& _gifThumbnail
408 			&& _gifThumbnail->ready()
409 			&& !_gifThumbnail->started()) {
410 			startGifAnimation(_gifThumbnail);
411 		}
412 		update();
413 	} break;
414 
415 	case NotificationRepaint: {
416 		if ((_gif && _gif->started() && !_gif->currentDisplayed())
417 			|| (_gifThumbnail
418 				&& _gifThumbnail->started()
419 				&& !_gifThumbnail->currentDisplayed())) {
420 			update(updateArea());
421 		}
422 	} break;
423 	}
424 }
425 
~MediaPreviewWidget()426 MediaPreviewWidget::~MediaPreviewWidget() {
427 }
428 
429 } // namespace Window
430