From b6edf4561d7c27a7ccb60899f7721221184aa1b1 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 29 Jan 2019 10:29:38 +0300
Subject: [PATCH] Add support for pattern wallpapers.

---
 Telegram/Resources/langs/lang.strings         |   4 +-
 Telegram/SourceFiles/boxes/background_box.cpp | 122 +++++++--
 Telegram/SourceFiles/boxes/background_box.h   |   6 +-
 Telegram/SourceFiles/boxes/boxes.style        |   2 +-
 .../SourceFiles/core/local_url_handlers.cpp   |   2 +-
 Telegram/SourceFiles/mainwidget.cpp           |   2 +-
 .../SourceFiles/settings/settings_chat.cpp    |   2 +-
 Telegram/SourceFiles/storage/localstorage.cpp | 100 +++++--
 Telegram/SourceFiles/storage/localstorage.h   |   2 +-
 Telegram/SourceFiles/ui/style/style_core.cpp  |   4 +
 .../SourceFiles/window/section_widget.cpp     |   2 +-
 .../window/themes/window_theme.cpp            | 243 ++++++++++++++----
 .../SourceFiles/window/themes/window_theme.h  |  17 +-
 13 files changed, 401 insertions(+), 107 deletions(-)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 60a871e45..94cb291f1 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -398,8 +398,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_theme_keep_changes" = "Keep changes";
 "lng_theme_revert" = "Revert";
 "lng_background_header" = "Background preview";
-"lng_background_text1" = "You can't swipe left or right to preview anything - this is tdesktop, sorry.";
-"lng_background_text2" = "Sounds awful.";
+"lng_background_text1" = "Ah, you kids today with techno music! You should enjoy the classics, like Hasselhoff!";
+"lng_background_text2" = "I can't even take you seriously right now.";
 "lng_background_bad_link" = "This background link appears to be invalid.";
 "lng_background_apply" = "Apply";
 "lng_background_share" = "Share";
diff --git a/Telegram/SourceFiles/boxes/background_box.cpp b/Telegram/SourceFiles/boxes/background_box.cpp
index 187453127..df01db3ea 100644
--- a/Telegram/SourceFiles/boxes/background_box.cpp
+++ b/Telegram/SourceFiles/boxes/background_box.cpp
@@ -74,9 +74,9 @@ AdminLog::OwnedItem GenerateTextItem(
 	return AdminLog::OwnedItem(delegate, item);
 }
 
-QImage PrepareScaledFromFull(
+QImage PrepareScaledNonPattern(
 		const QImage &image,
-		Images::Option blur = Images::Option(0)) {
+		Images::Option blur) {
 	const auto size = st::boxWideWidth;
 	const auto width = std::max(image.width(), 1);
 	const auto height = std::max(image.height(), 1);
@@ -90,17 +90,71 @@ QImage PrepareScaledFromFull(
 		image,
 		takeWidth,
 		takeHeight,
-		Images::Option::Smooth | blur,
+		Images::Option::Smooth
+		| Images::Option::TransparentBackground
+		| blur,
 		size,
 		size);
 }
 
-QPixmap PrepareScaledFromThumb(not_null<Image*> thumb, bool good) {
-	return thumb->loaded()
-		? App::pixmapFromImageInPlace(PrepareScaledFromFull(
-			thumb->original(),
-			good ? Images::Option(0) : Images::Option::Blurred))
-		: QPixmap();
+QImage ColorizePattern(QImage image, QColor color) {
+	if (image.format() != QImage::Format_ARGB32_Premultiplied) {
+		image = std::move(image).convertToFormat(
+			QImage::Format_ARGB32_Premultiplied);
+	}
+	// Similar to style::colorizeImage.
+	// But style::colorizeImage takes pattern with all pixels having the
+	// same components value, from (0, 0, 0, 0) to (255, 255, 255, 255).
+	//
+	// While in patterns we have different value ranges, usually they are
+	// from (0, 0, 0, 0) to (0, 0, 0, 255), so we should use only 'alpha'.
+
+	const auto width = image.width();
+	const auto height = image.height();
+	const auto pattern = anim::shifted(color);
+
+	const auto resultBytesPerPixel = (image.depth() >> 3);
+	constexpr auto resultIntsPerPixel = 1;
+	const auto resultIntsPerLine = (image.bytesPerLine() >> 2);
+	const auto resultIntsAdded = resultIntsPerLine - width * resultIntsPerPixel;
+	auto resultInts = reinterpret_cast<uint32*>(image.bits());
+	Assert(resultIntsAdded >= 0);
+	Assert(image.depth() == static_cast<int>((resultIntsPerPixel * sizeof(uint32)) << 3));
+	Assert(image.bytesPerLine() == (resultIntsPerLine << 2));
+
+	const auto maskBytesPerPixel = (image.depth() >> 3);
+	const auto maskBytesPerLine = image.bytesPerLine();
+	const auto maskBytesAdded = maskBytesPerLine - width * maskBytesPerPixel;
+
+	// We want to read the last byte of four available.
+	// This is the difference with style::colorizeImage.
+	auto maskBytes = image.constBits() + (maskBytesPerPixel - 1);
+	Assert(maskBytesAdded >= 0);
+	Assert(image.depth() == (maskBytesPerPixel << 3));
+	for (auto y = 0; y != height; ++y) {
+		for (auto x = 0; x != width; ++x) {
+			auto maskOpacity = static_cast<anim::ShiftedMultiplier>(*maskBytes) + 1;
+			*resultInts = anim::unshifted(pattern * maskOpacity);
+			maskBytes += maskBytesPerPixel;
+			resultInts += resultIntsPerPixel;
+		}
+		maskBytes += maskBytesAdded;
+		resultInts += resultIntsAdded;
+	}
+	return std::move(image);
+}
+
+QImage PrepareScaledFromFull(
+		const QImage &image,
+		std::optional<QColor> patternBackground,
+		Images::Option blur = Images::Option(0)) {
+	auto result = PrepareScaledNonPattern(image, blur);
+	if (patternBackground) {
+		result = ColorizePattern(
+			std::move(result),
+			Window::Theme::PatternColor(*patternBackground));
+	}
+	return result;
 }
 
 } // namespace
@@ -358,10 +412,11 @@ void BackgroundPreviewBox::paintEvent(QPaintEvent *e) {
 	Painter p(this);
 
 	const auto ms = getms();
-
-	if (const auto color = _paper.backgroundColor()) {
+	const auto color = _paper.backgroundColor();
+	if (color) {
 		p.fillRect(e->rect(), *color);
-	} else {
+	}
+	if (!color || _paper.isPattern()) {
 		if (_scaled.isNull() && !setScaledFromThumb()) {
 			p.fillRect(e->rect(), st::boxBg);
 			return;
@@ -375,6 +430,11 @@ void BackgroundPreviewBox::paintEvent(QPaintEvent *e) {
 void BackgroundPreviewBox::paintImage(Painter &p) {
 	Expects(!_scaled.isNull());
 
+	p.setOpacity(_paper.isPattern()
+		? std::clamp(_paper.patternIntensity() / 100., 0., 1.)
+		: 1.);
+	const auto guard = gsl::finally([&] { p.setOpacity(1.); });
+
 	const auto factor = cIntRetinaFactor();
 	const auto size = st::boxWideWidth;
 	const auto from = QRect(
@@ -457,10 +517,25 @@ void BackgroundPreviewBox::step_radial(TimeMs ms, bool timer) {
 }
 
 bool BackgroundPreviewBox::setScaledFromThumb() {
-	_scaled = PrepareScaledFromThumb(
-		_paper.thumbnail(),
-		!_paper.document());
-	return !_scaled.isNull();
+	Expects(_paper.thumbnail() != nullptr);
+
+	const auto thumbnail = _paper.thumbnail();
+	if (!thumbnail->loaded()) {
+		return false;
+	}
+	setScaledFromImage(PrepareScaledFromFull(
+		thumbnail->original(),
+		patternBackgroundColor(),
+		_paper.document() ? Images::Option::Blurred : Images::Option(0)));
+	return true;
+}
+
+void BackgroundPreviewBox::setScaledFromImage(QImage &&image) {
+	_scaled = App::pixmapFromImageInPlace(std::move(image));
+}
+
+std::optional<QColor> BackgroundPreviewBox::patternBackgroundColor() const {
+	return _paper.isPattern() ? _paper.backgroundColor() : std::nullopt;
 }
 
 void BackgroundPreviewBox::checkLoadedDocument() {
@@ -477,9 +552,10 @@ void BackgroundPreviewBox::checkLoadedDocument() {
 		crl::async([
 			this,
 			image = std::move(image),
+			patternBackground = patternBackgroundColor(),
 			guard = std::move(right)
 		]() mutable {
-			auto scaled = PrepareScaledFromFull(image);
+			auto scaled = PrepareScaledFromFull(image, patternBackground);
 			crl::on_main([
 				this,
 				image = std::move(image),
@@ -489,7 +565,7 @@ void BackgroundPreviewBox::checkLoadedDocument() {
 				if (!guard) {
 					return;
 				}
-				_scaled = App::pixmapFromImageInPlace(std::move(scaled));
+				setScaledFromImage(std::move(scaled));
 				_full = std::move(image);
 				update();
 			});
@@ -497,17 +573,19 @@ void BackgroundPreviewBox::checkLoadedDocument() {
 	});
 }
 
-bool BackgroundPreviewBox::Start(const QString &slug, const QString &mode) {
+bool BackgroundPreviewBox::Start(
+		const QString &slug,
+		const QMap<QString, QString> &params) {
 	if (const auto paper = Data::WallPaper::FromColorSlug(slug)) {
-		Ui::show(Box<BackgroundPreviewBox>(*paper));
+		Ui::show(Box<BackgroundPreviewBox>(paper->withUrlParams(params)));
 		return true;
 	}
 	if (!IsValidWallPaperSlug(slug)) {
 		Ui::show(Box<InformBox>(lang(lng_background_bad_link)));
 		return false;
 	}
-	Auth().api().requestWallPaper(slug, [](const Data::WallPaper &result) {
-		Ui::show(Box<BackgroundPreviewBox>(result));
+	Auth().api().requestWallPaper(slug, [=](const Data::WallPaper &result) {
+		Ui::show(Box<BackgroundPreviewBox>(result.withUrlParams(params)));
 	}, [](const RPCError &error) {
 		Ui::show(Box<InformBox>(lang(lng_background_bad_link)));
 	});
diff --git a/Telegram/SourceFiles/boxes/background_box.h b/Telegram/SourceFiles/boxes/background_box.h
index 401521dd9..44d7156d0 100644
--- a/Telegram/SourceFiles/boxes/background_box.h
+++ b/Telegram/SourceFiles/boxes/background_box.h
@@ -39,7 +39,9 @@ class BackgroundPreviewBox
 public:
 	BackgroundPreviewBox(QWidget*, const Data::WallPaper &paper);
 
-	static bool Start(const QString &slug, const QString &mode);
+	static bool Start(
+		const QString &slug,
+		const QMap<QString, QString> &params);
 
 	using Element = HistoryView::Element;
 	HistoryView::Context elementContext() override;
@@ -67,6 +69,8 @@ private:
 
 	void checkLoadedDocument();
 	bool setScaledFromThumb();
+	void setScaledFromImage(QImage &&image);
+	std::optional<QColor> patternBackgroundColor() const;
 	void paintImage(Painter &p);
 	void paintRadial(Painter &p, TimeMs ms);
 	void paintTexts(Painter &p, TimeMs ms);
diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style
index 439d71c21..1d762c705 100644
--- a/Telegram/SourceFiles/boxes/boxes.style
+++ b/Telegram/SourceFiles/boxes/boxes.style
@@ -69,7 +69,7 @@ boxTitle: FlatLabel(defaultFlatLabel) {
 		linkFontOver: font(17px semibold underline);
 	}
 }
-boxTitlePosition: point(23px, 20px);
+boxTitlePosition: point(23px, 16px);
 boxTitleHeight: 56px;
 boxLayerTitlePosition: point(23px, 16px);
 boxLayerTitleHeight: 56px;
diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp
index 719d4db66..ac22b9bf9 100644
--- a/Telegram/SourceFiles/core/local_url_handlers.cpp
+++ b/Telegram/SourceFiles/core/local_url_handlers.cpp
@@ -173,7 +173,7 @@ bool ShowWallPaper(const Match &match, const QVariant &context) {
 		qthelp::UrlParamNameTransform::ToLower);
 	return BackgroundPreviewBox::Start(
 		params.value(qsl("slug")),
-		params.value(qsl("mode")));
+		params);
 }
 
 bool ResolveUsername(const Match &match, const QVariant &context) {
diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp
index d1be15b0d..6f53d1ec6 100644
--- a/Telegram/SourceFiles/mainwidget.cpp
+++ b/Telegram/SourceFiles/mainwidget.cpp
@@ -1057,7 +1057,7 @@ bool MainWidget::sendMessageFail(const RPCError &error) {
 }
 
 void MainWidget::cacheBackground() {
-	if (Window::Theme::Background()->color()) {
+	if (Window::Theme::Background()->colorForFill()) {
 		return;
 	} else if (Window::Theme::Background()->tile()) {
 		auto &bg = Window::Theme::Background()->pixmapForTiled();
diff --git a/Telegram/SourceFiles/settings/settings_chat.cpp b/Telegram/SourceFiles/settings/settings_chat.cpp
index e701ecf50..78239487e 100644
--- a/Telegram/SourceFiles/settings/settings_chat.cpp
+++ b/Telegram/SourceFiles/settings/settings_chat.cpp
@@ -265,7 +265,7 @@ void BackgroundRow::updateImage() {
 		Painter p(&back);
 		PainterHighQualityEnabler hq(p);
 
-		if (const auto color = Window::Theme::Background()->color()) {
+		if (const auto color = Window::Theme::Background()->colorForFill()) {
 			p.fillRect(
 				0,
 				0,
diff --git a/Telegram/SourceFiles/storage/localstorage.cpp b/Telegram/SourceFiles/storage/localstorage.cpp
index 383478ddd..2368346a2 100644
--- a/Telegram/SourceFiles/storage/localstorage.cpp
+++ b/Telegram/SourceFiles/storage/localstorage.cpp
@@ -48,9 +48,11 @@ constexpr auto kFileLoaderQueueStopTimeout = TimeMs(5000);
 constexpr auto kDefaultStickerInstallDate = TimeId(1);
 constexpr auto kProxyTypeShift = 1024;
 constexpr auto kWriteMapTimeout = TimeMs(1000);
+constexpr auto kSavedBackgroundFormat = QImage::Format_ARGB32_Premultiplied;
 
 constexpr auto kWallPaperLegacySerializeTagId = int32(-111);
 constexpr auto kWallPaperSerializeTagId = int32(-112);
+constexpr auto kWallPaperSidesLimit = 10000;
 
 constexpr auto kSinglePeerTypeUser = qint32(1);
 constexpr auto kSinglePeerTypeChat = qint32(2);
@@ -3960,7 +3962,7 @@ void readSavedGifs() {
 	}
 }
 
-void writeBackground(const Data::WallPaper &paper, const QImage &img) {
+void writeBackground(const Data::WallPaper &paper, const QImage &image) {
 	if (!_working() || !_backgroundCanWrite) {
 		return;
 	}
@@ -3973,11 +3975,33 @@ void writeBackground(const Data::WallPaper &paper, const QImage &img) {
 	auto &backgroundKey = Window::Theme::IsNightMode()
 		? _backgroundKeyNight
 		: _backgroundKeyDay;
-	QByteArray bmp;
-	if (!img.isNull()) {
-		QBuffer buf(&bmp);
-		if (!img.save(&buf, "BMP")) {
-			return;
+	auto imageData = QByteArray();
+	if (!image.isNull()) {
+		const auto width = qint32(image.width());
+		const auto height = qint32(image.height());
+		const auto perpixel = (image.depth() >> 3);
+		const auto srcperline = image.bytesPerLine();
+		const auto srcsize = srcperline * height;
+		const auto dstperline = width * perpixel;
+		const auto dstsize = dstperline * height;
+		const auto copy = (image.format() != kSavedBackgroundFormat)
+			? image.convertToFormat(kSavedBackgroundFormat)
+			: image;
+		imageData.resize(2 * sizeof(qint32) + dstsize);
+
+		auto dst = bytes::make_detached_span(imageData);
+		bytes::copy(dst, bytes::object_as_span(&width));
+		dst = dst.subspan(sizeof(qint32));
+		bytes::copy(dst, bytes::object_as_span(&height));
+		dst = dst.subspan(sizeof(qint32));
+		const auto src = bytes::make_span(image.constBits(), srcsize);
+		if (srcsize == dstsize) {
+			bytes::copy(dst, src);
+		} else {
+			for (auto y = 0; y != height; ++y) {
+				bytes::copy(dst, src.subspan(y * srcperline, dstperline));
+				dst = dst.subspan(dstperline);
+			}
 		}
 	}
 	if (!backgroundKey) {
@@ -3988,20 +4012,12 @@ void writeBackground(const Data::WallPaper &paper, const QImage &img) {
 	const auto serialized = paper.serialize();
 	quint32 size = sizeof(qint32)
 		+ Serialize::bytearraySize(serialized)
-		+ Serialize::bytearraySize(bmp);
+		+ Serialize::bytearraySize(imageData);
 	EncryptedDescriptor data(size);
 	data.stream
 		<< qint32(kWallPaperSerializeTagId)
 		<< serialized
-		<< bmp;
-	//+2 * sizeof(quint64)
-	//	+ sizeof(quint32)
-	//	+ Serialize::stringSize(paper.slug)
-
-	//	<< quint64(paper.id)
-	//	<< quint64(paper.accessHash)
-	//	<< quint32(paper.flags.value())
-	//	<< paper.slug
+		<< imageData;
 
 	FileWriteDescriptor file(backgroundKey);
 	file.writeEncrypted(data);
@@ -4052,8 +4068,8 @@ bool readBackground() {
 		return false;
 	}
 
-	QByteArray bmp;
-	bg.stream >> bmp;
+	QByteArray imageData;
+	bg.stream >> imageData;
 	const auto isOldEmptyImage = (bg.stream.status() != QDataStream::Ok);
 	if (isOldEmptyImage
 		|| Data::IsLegacy1DefaultWallPaper(*paper)
@@ -4067,20 +4083,56 @@ bool readBackground() {
 		}
 		_backgroundCanWrite = true;
 		return true;
-	} else if (Data::IsThemeWallPaper(*paper) && bmp.isEmpty()) {
+	} else if (Data::IsThemeWallPaper(*paper) && imageData.isEmpty()) {
 		_backgroundCanWrite = false;
 		Window::Theme::Background()->setImage(*paper);
 		_backgroundCanWrite = true;
 		return true;
 	}
-
 	auto image = QImage();
-	auto buffer = QBuffer(&bmp);
-	auto reader = QImageReader(&buffer);
+	if (legacyId == kWallPaperSerializeTagId) {
+		const auto perpixel = 4;
+		auto src = bytes::make_span(imageData);
+		auto width = qint32();
+		auto height = qint32();
+		if (src.size() > 2 * sizeof(qint32)) {
+			bytes::copy(
+				bytes::object_as_span(&width),
+				src.subspan(0, sizeof(qint32)));
+			src = src.subspan(sizeof(qint32));
+			bytes::copy(
+				bytes::object_as_span(&height),
+				src.subspan(0, sizeof(qint32)));
+			src = src.subspan(sizeof(qint32));
+			if (width + height <= kWallPaperSidesLimit
+				&& src.size() == width * height * perpixel) {
+				image = QImage(
+					width,
+					height,
+					QImage::Format_ARGB32_Premultiplied);
+				if (!image.isNull()) {
+					const auto srcperline = width * perpixel;
+					const auto srcsize = srcperline * height;
+					const auto dstperline = image.bytesPerLine();
+					const auto dstsize = dstperline * height;
+					Assert(srcsize == dstsize);
+					bytes::copy(
+						bytes::make_span(image.bits(), dstsize),
+						src);
+				}
+			}
+		}
+	} else {
+		auto buffer = QBuffer(&imageData);
+		auto reader = QImageReader(&buffer);
 #ifndef OS_MAC_OLD
-	reader.setAutoTransform(true);
+		reader.setAutoTransform(true);
 #endif // OS_MAC_OLD
-	if (reader.read(&image) || paper->backgroundColor()) {
+		if (!reader.read(&image)) {
+			image = QImage();
+		}
+	}
+	if (!image.isNull()|| paper->backgroundColor()) {
 		_backgroundCanWrite = false;
 		Window::Theme::Background()->setImage(*paper, std::move(image));
 		_backgroundCanWrite = true;
diff --git a/Telegram/SourceFiles/storage/localstorage.h b/Telegram/SourceFiles/storage/localstorage.h
index 6b562eedb..8536c2dd5 100644
--- a/Telegram/SourceFiles/storage/localstorage.h
+++ b/Telegram/SourceFiles/storage/localstorage.h
@@ -140,7 +140,7 @@ void writeSavedGifs();
 void readSavedGifs();
 int32 countSavedGifsHash();
 
-void writeBackground(const Data::WallPaper &paper, const QImage &img);
+void writeBackground(const Data::WallPaper &paper, const QImage &image);
 bool readBackground();
 
 void writeTheme(const Window::Theme::Saved &saved);
diff --git a/Telegram/SourceFiles/ui/style/style_core.cpp b/Telegram/SourceFiles/ui/style/style_core.cpp
index b518599b0..287efde11 100644
--- a/Telegram/SourceFiles/ui/style/style_core.cpp
+++ b/Telegram/SourceFiles/ui/style/style_core.cpp
@@ -62,6 +62,10 @@ void stopManager() {
 }
 
 void colorizeImage(const QImage &src, QColor c, QImage *outResult, QRect srcRect, QPoint dstPoint) {
+	// In background_box ColorizePattern we use the fact that
+	// colorizeImage takes only first byte of the mask, so it
+	// could be used for wallpaper patterns, which have values
+	// in ranges (0, 0, 0, 0) to (0, 0, 0, 255) (only 'alpha').
 	if (srcRect.isNull()) {
 		srcRect = src.rect();
 	} else {
diff --git a/Telegram/SourceFiles/window/section_widget.cpp b/Telegram/SourceFiles/window/section_widget.cpp
index fd5bc33c3..010a88e00 100644
--- a/Telegram/SourceFiles/window/section_widget.cpp
+++ b/Telegram/SourceFiles/window/section_widget.cpp
@@ -77,7 +77,7 @@ void SectionWidget::PaintBackground(QWidget *widget, QPaintEvent *event) {
 
 	auto clip = event->rect();
 	auto fill = QRect(0, 0, widget->width(), App::main()->height());
-	if (const auto color = Window::Theme::Background()->color()) {
+	if (const auto color = Window::Theme::Background()->colorForFill()) {
 		p.fillRect(fill, *color);
 		return;
 	}
diff --git a/Telegram/SourceFiles/window/themes/window_theme.cpp b/Telegram/SourceFiles/window/themes/window_theme.cpp
index a53fed9de..8fc55838f 100644
--- a/Telegram/SourceFiles/window/themes/window_theme.cpp
+++ b/Telegram/SourceFiles/window/themes/window_theme.cpp
@@ -75,6 +75,92 @@ std::optional<QColor> MaybeColorFromSerialized(quint32 serialized) {
 			int(serialized & 0xFFU)));
 }
 
+std::optional<QColor> ColorFromString(const QString &string) {
+	if (string.size() != 6) {
+		return {};
+	} else if (ranges::find_if(string, [](QChar ch) {
+		return (ch < 'a' || ch > 'f')
+			&& (ch < 'A' || ch > 'F')
+			&& (ch < '0' || ch > '9');
+	}) != string.end()) {
+		return {};
+	}
+	const auto component = [](const QString &text, int index) {
+		const auto decimal = [](QChar hex) {
+			const auto code = hex.unicode();
+			return (code >= '0' && code <= '9')
+				? int(code - '0')
+				: (code >= 'a' && code <= 'f')
+				? int(code - 'a' + 0x0a)
+				: int(code - 'A' + 0x0a);
+		};
+		index *= 2;
+		return decimal(text[index]) * 0x10 + decimal(text[index + 1]);
+	};
+	return QColor(
+		component(string, 0),
+		component(string, 1),
+		component(string, 2),
+		255);
+}
+
+QImage PreparePatternImage(QImage image, QColor bg, QColor fg, int intensity) {
+	if (image.format() != QImage::Format_ARGB32_Premultiplied) {
+		image = std::move(image).convertToFormat(
+			QImage::Format_ARGB32_Premultiplied);
+	}
+	// Similar to ColorizePattern.
+	// But here we set bg to all 'alpha=0' pixels and fg to opaque ones.
+
+	const auto width = image.width();
+	const auto height = image.height();
+	const auto alpha = anim::interpolate(
+		0,
+		255,
+		fg.alphaF() * std::clamp(intensity / 100., 0., 1.));
+	if (!alpha) {
+		image.fill(bg);
+		return std::move(image);
+	}
+	fg.setAlpha(255);
+	const auto patternBg = anim::shifted(bg);
+	const auto patternFg = anim::shifted(fg);
+
+	const auto resultBytesPerPixel = (image.depth() >> 3);
+	constexpr auto resultIntsPerPixel = 1;
+	const auto resultIntsPerLine = (image.bytesPerLine() >> 2);
+	const auto resultIntsAdded = resultIntsPerLine - width * resultIntsPerPixel;
+	auto resultInts = reinterpret_cast<uint32*>(image.bits());
+	Assert(resultIntsAdded >= 0);
+	Assert(image.depth() == static_cast<int>((resultIntsPerPixel * sizeof(uint32)) << 3));
+	Assert(image.bytesPerLine() == (resultIntsPerLine << 2));
+
+	const auto maskBytesPerPixel = (image.depth() >> 3);
+	const auto maskBytesPerLine = image.bytesPerLine();
+	const auto maskBytesAdded = maskBytesPerLine - width * maskBytesPerPixel;
+
+	// We want to read the last byte of four available.
+	// This is the difference with style::colorizeImage.
+	auto maskBytes = image.constBits() + (maskBytesPerPixel - 1);
+	Assert(maskBytesAdded >= 0);
+	Assert(image.depth() == (maskBytesPerPixel << 3));
+	for (auto y = 0; y != height; ++y) {
+		for (auto x = 0; x != width; ++x) {
+			const auto maskOpacity = static_cast<anim::ShiftedMultiplier>(
+				*maskBytes) + 1;
+			const auto fgOpacity = (maskOpacity * alpha) >> 8;
+			const auto bgOpacity = 256 - fgOpacity;
+			*resultInts = anim::unshifted(
+				patternBg * bgOpacity + patternFg * fgOpacity);
+			maskBytes += maskBytesPerPixel;
+			resultInts += resultIntsPerPixel;
+		}
+		maskBytes += maskBytesAdded;
+		resultInts += resultIntsAdded;
+	}
+	return std::move(image);
+}
+
 } // namespace
 
 WallPaper::WallPaper(WallPaperId id) : _id(id) {
@@ -105,6 +191,22 @@ Image *WallPaper::thumbnail() const {
 	return _thumbnail;
 }
 
+bool WallPaper::isPattern() const {
+	return _flags & MTPDwallPaper::Flag::f_pattern;
+}
+
+bool WallPaper::isDefault() const {
+	return _flags & MTPDwallPaper::Flag::f_default;
+}
+
+bool WallPaper::isCreator() const {
+	return _flags & MTPDwallPaper::Flag::f_creator;
+}
+
+int WallPaper::patternIntensity() const {
+	return _intensity;
+}
+
 bool WallPaper::hasShareUrl() const {
 	return !_slug.isEmpty();
 }
@@ -131,6 +233,39 @@ FileOrigin WallPaper::fileOrigin() const {
 	return FileOriginWallpaper(_id, _accessHash);
 }
 
+WallPaper WallPaper::withUrlParams(
+		const QMap<QString, QString> &params) const {
+	using Flag = MTPDwallPaperSettings::Flag;
+
+	auto result = *this;
+	result._settings = Flag(0);
+	result._backgroundColor = ColorFromString(_slug);
+	result._intensity = kDefaultIntensity;
+
+	if (auto mode = params.value("mode"); !mode.isEmpty()) {
+		const auto list = mode.replace('+', ' ').split(' ');
+		for (const auto &change : list) {
+			if (change == qstr("blur")) {
+				result._settings |= Flag::f_blur;
+			} else if (change == qstr("motion")) {
+				result._settings |= Flag::f_motion;
+			}
+		}
+	}
+	if (const auto color = ColorFromString(params.value("bg_color"))) {
+		result._backgroundColor = color;
+	}
+	if (const auto string = params.value("intensity"); !string.isEmpty()) {
+		auto ok = false;
+		const auto intensity = string.toInt(&ok);
+		if (ok && base::in_range(intensity, 0, 100)) {
+			result._intensity = intensity;
+		}
+	}
+
+	return result;
+}
+
 std::optional<WallPaper> WallPaper::Create(const MTPWallPaper &data) {
 	return data.match([](const MTPDwallPaper &data) {
 		return Create(data);
@@ -138,6 +273,8 @@ std::optional<WallPaper> WallPaper::Create(const MTPWallPaper &data) {
 }
 
 std::optional<WallPaper> WallPaper::Create(const MTPDwallPaper &data) {
+	using Flag = MTPDwallPaper::Flag;
+
 	const auto document = Auth().data().processDocument(
 		data.vdocument);
 	if (!document->checkWallPaperProperties()) {
@@ -150,14 +287,21 @@ std::optional<WallPaper> WallPaper::Create(const MTPDwallPaper &data) {
 	result._document = document;
 	result._thumbnail = document->thumbnail();
 	if (data.has_settings()) {
+		const auto isPattern = ((result._flags & Flag::f_pattern) != 0);
 		data.vsettings.match([&](const MTPDwallPaperSettings &data) {
+			using Flag = MTPDwallPaperSettings::Flag;
+
 			result._settings = data.vflags.v;
-			if (data.has_background_color()) {
+			if (isPattern && data.has_background_color()) {
 				result._backgroundColor = MaybeColorFromSerialized(
 					data.vbackground_color.v);
+			} else {
+				result._settings &= ~Flag::f_background_color;
 			}
-			if (data.has_intensity()) {
+			if (isPattern && data.has_intensity()) {
 				result._intensity = data.vintensity.v;
+			} else {
+				result._settings &= ~Flag::f_intensity;
 			}
 		});
 	}
@@ -203,7 +347,6 @@ std::optional<WallPaper> WallPaper::FromSerialized(
 	auto settings = qint32();
 	auto backgroundColor = quint32();
 	auto intensity = qint32();
-	auto documentId = quint64();
 
 	auto stream = QDataStream(serialized);
 	stream.setVersion(QDataStream::Qt_5_1);
@@ -214,8 +357,7 @@ std::optional<WallPaper> WallPaper::FromSerialized(
 		>> slug
 		>> settings
 		>> backgroundColor
-		>> intensity
-		>> documentId;
+		>> intensity;
 	if (stream.status() != QDataStream::Ok) {
 		return std::nullopt;
 	} else if (intensity < 0 || intensity > 100) {
@@ -243,7 +385,7 @@ std::optional<WallPaper> WallPaper::FromLegacySerialized(
 	result._accessHash = accessHash;
 	result._flags = MTPDwallPaper::Flags::from_raw(flags);
 	result._slug = slug;
-	result._backgroundColor = Window::Theme::GetWallPaperColor(slug);
+	result._backgroundColor = ColorFromString(slug);
 	if (!ValidateFlags(result._flags)) {
 		return std::nullopt;
 	}
@@ -259,7 +401,7 @@ std::optional<WallPaper> WallPaper::FromLegacyId(qint32 legacyId) {
 }
 
 std::optional<WallPaper> WallPaper::FromColorSlug(const QString &slug) {
-	if (const auto color = Window::Theme::GetWallPaperColor(slug)) {
+	if (const auto color = ColorFromString(slug)) {
 		auto result = CustomWallPaper();
 		result._slug = slug;
 		result._backgroundColor = color;
@@ -707,6 +849,11 @@ void ChatBackground::start() {
 void ChatBackground::setImage(
 		const Data::WallPaper &paper,
 		QImage &&image) {
+	if (image.format() != QImage::Format_ARGB32_Premultiplied) {
+		image = std::move(image).convertToFormat(
+			QImage::Format_ARGB32_Premultiplied);
+	}
+
 	const auto needResetAdjustable = Data::IsDefaultWallPaper(paper)
 		&& !Data::IsDefaultWallPaper(_paper)
 		&& !nightMode()
@@ -744,7 +891,7 @@ void ChatBackground::setImage(
 					Qt::SmoothTransformation);
 			}
 		} else if (Data::IsDefaultWallPaper(_paper)
-			|| (!color() && image.isNull())) {
+			|| (!_paper.backgroundColor() && image.isNull())) {
 			setPaper(Data::DefaultWallPaper());
 			image.load(qsl(":/gui/art/bg.jpg"));
 		}
@@ -754,15 +901,26 @@ void ChatBackground::setImage(
 				|| Data::IsLegacy1DefaultWallPaper(_paper))
 				? QImage()
 				: image));
-		if (const auto fill = color()) {
-			if (adjustPaletteRequired()) {
-				adjustPaletteUsingColor(*fill);
+		if (const auto fill = _paper.backgroundColor()) {
+			if (_paper.isPattern() && !image.isNull()) {
+				setPreparedImage(Data::PreparePatternImage(
+					std::move(image),
+					*fill,
+					PatternColor(*fill),
+					_paper.patternIntensity()));
+			} else {
+				_pixmap = QPixmap();
+				_pixmapForTiled = QPixmap();
+				if (adjustPaletteRequired()) {
+					adjustPaletteUsingColor(*fill);
+				}
 			}
 		} else {
 			setPreparedImage(prepareBackgroundImage(std::move(image)));
 		}
 	}
-	Assert((!_pixmap.isNull() && !_pixmapForTiled.isNull()) || color());
+	Assert((!_pixmap.isNull() && !_pixmapForTiled.isNull())
+		|| colorForFill());
 
 	notify(BackgroundUpdate(BackgroundUpdate::Type::New, tile()));
 	if (needResetAdjustable) {
@@ -865,15 +1023,19 @@ void ChatBackground::adjustPaletteUsingBackground(const QImage &img) {
 }
 
 void ChatBackground::adjustPaletteUsingColor(QColor color) {
-	auto hue = color.hslHueF();
-	auto saturation = color.hslSaturationF();
+	const auto hue = color.hslHueF();
+	const auto saturation = color.hslSaturationF();
 	for (const auto &color : _adjustableColors) {
 		adjustColor(color.item, hue, saturation);
 	}
 }
 
+std::optional<QColor> ChatBackground::colorForFill() const {
+	return _pixmap.isNull() ? _paper.backgroundColor() : std::nullopt;
+}
+
 QImage ChatBackground::createCurrentImage() const {
-	if (const auto fill = color()) {
+	if (const auto fill = colorForFill()) {
 		auto result = QImage(
 			kMinimumTiledSize,
 			kMinimumTiledSize,
@@ -881,7 +1043,7 @@ QImage ChatBackground::createCurrentImage() const {
 		result.fill(*fill);
 		return result;
 	}
-	return pixmap().toImage();
+	return pixmap().toImage(); // #TODO patterns
 }
 
 bool ChatBackground::tile() const {
@@ -909,7 +1071,7 @@ bool ChatBackground::tileNight() const {
 }
 
 void ChatBackground::ensureStarted() {
-	if (_pixmap.isNull()) {
+	if (_pixmap.isNull() && !_paper.backgroundColor()) {
 		// We should start first, otherwise the default call
 		// to start() will reset this value to _themeTile.
 		start();
@@ -1019,6 +1181,7 @@ void ChatBackground::setTestingTheme(Instance &&theme) {
 		setTile(theme.tiled);
 	} else {
 		// Apply current background image so that service bg colors are recounted.
+		// #TODO patterns
 		setImage(_paper, std::move(_pixmap).toImage());
 	}
 	notify(BackgroundUpdate(BackgroundUpdate::Type::TestingTheme, tile()), true);
@@ -1086,7 +1249,7 @@ void ChatBackground::writeNewBackgroundSettings() {
 		((Data::IsThemeWallPaper(_paper)
 			|| Data::IsDefaultWallPaper(_paper))
 			? QImage()
-			: _pixmap.toImage()));
+			: _pixmap.toImage())); // #TODO patterns
 }
 
 void ChatBackground::revert() {
@@ -1097,6 +1260,7 @@ void ChatBackground::revert() {
 		setImage(_paperForRevert, std::move(_imageForRevert));
 	} else {
 		// Apply current background image so that service bg colors are recounted.
+		// #TODO patterns
 		setImage(_paper, std::move(_pixmap).toImage());
 	}
 	notify(BackgroundUpdate(BackgroundUpdate::Type::RevertingTheme, tile()), true);
@@ -1335,35 +1499,6 @@ bool IsPaletteTestingPath(const QString &path) {
 	return false;
 }
 
-std::optional<QColor> GetWallPaperColor(const QString &slug) {
-	if (slug.size() != 6) {
-		return {};
-	} else if (ranges::find_if(slug, [](QChar ch) {
-		return (ch < 'a' || ch > 'f')
-			&& (ch < 'A' || ch > 'F')
-			&& (ch < '0' || ch > '9');
-	}) != slug.end()) {
-		return {};
-	}
-	const auto component = [](const QString &text, int index) {
-		const auto decimal = [](QChar hex) {
-			const auto code = hex.unicode();
-			return (code >= '0' && code <= '9')
-				? int(code - '0')
-				: (code >= 'a' && code <= 'f')
-				? int(code - 'a' + 0x0a)
-				: int(code - 'A' + 0x0a);
-		};
-		index *= 2;
-		return decimal(text[index]) * 0x10 + decimal(text[index + 1]);
-	};
-	return QColor(
-		component(slug, 0),
-		component(slug, 1),
-		component(slug, 2),
-		255);
-}
-
 void ComputeBackgroundRects(QRect wholeFill, QSize imageSize, QRect &to, QRect &from) {
 	if (uint64(imageSize.width()) * wholeFill.height() > uint64(imageSize.height()) * wholeFill.width()) {
 		float64 pxsize = wholeFill.height() / float64(imageSize.height());
@@ -1444,5 +1579,19 @@ bool ReadPaletteValues(const QByteArray &content, Fn<bool(QLatin1String name, QL
 	return true;
 }
 
+QColor PatternColor(QColor background) {
+	const auto hue = background.hueF();
+	const auto saturation = background.saturationF();
+	const auto value = background.valueF();
+	return QColor::fromHsvF(
+		hue,
+		std::min(1.0, saturation + 0.05 + 0.1 * (1. - saturation)),
+		(value > 0.5
+			? std::max(0., value * 0.65)
+			: std::max(0., std::min(1., 1. - value * 0.65))),
+		0.4
+	).toRgb();
+}
+
 } // namespace Theme
 } // namespace Window
diff --git a/Telegram/SourceFiles/window/themes/window_theme.h b/Telegram/SourceFiles/window/themes/window_theme.h
index a8cd498df..4b7646806 100644
--- a/Telegram/SourceFiles/window/themes/window_theme.h
+++ b/Telegram/SourceFiles/window/themes/window_theme.h
@@ -23,6 +23,10 @@ public:
 	[[nodiscard]] std::optional<QColor> backgroundColor() const;
 	[[nodiscard]] DocumentData *document() const;
 	[[nodiscard]] Image *thumbnail() const;
+	[[nodiscard]] bool isPattern() const;
+	[[nodiscard]] bool isDefault() const;
+	[[nodiscard]] bool isCreator() const;
+	[[nodiscard]] int patternIntensity() const;
 	[[nodiscard]] bool hasShareUrl() const;
 	[[nodiscard]] QString shareUrl() const;
 
@@ -30,6 +34,9 @@ public:
 	void loadThumbnail() const;
 	[[nodiscard]] FileOrigin fileOrigin() const;
 
+	[[nodiscard]] WallPaper withUrlParams(
+		const QMap<QString, QString> &params) const;
+
 	[[nodiscard]] static std::optional<WallPaper> Create(
 		const MTPWallPaper &data);
 	[[nodiscard]] static std::optional<WallPaper> Create(
@@ -49,6 +56,8 @@ public:
 		const QString &slug);
 
 private:
+	static constexpr auto kDefaultIntensity = 40;
+
 	WallPaperId _id = WallPaperId();
 	uint64 _accessHash = 0;
 	MTPDwallPaper::Flags _flags;
@@ -56,7 +65,7 @@ private:
 
 	MTPDwallPaperSettings::Flags _settings;
 	std::optional<QColor> _backgroundColor;
-	int _intensity = 40;
+	int _intensity = kDefaultIntensity;
 
 	DocumentData *_document = nullptr;
 	Image *_thumbnail = nullptr;
@@ -138,7 +147,7 @@ void Revert();
 bool LoadFromFile(const QString &file, Instance *out, QByteArray *outContent);
 bool IsPaletteTestingPath(const QString &path);
 
-[[nodiscard]] std::optional<QColor> GetWallPaperColor(const QString &slug);
+QColor PatternColor(QColor background);
 
 struct BackgroundUpdate {
 	enum class Type {
@@ -190,9 +199,7 @@ public:
 	[[nodiscard]] const QPixmap &pixmapForTiled() const {
 		return _pixmapForTiled;
 	}
-	[[nodiscard]] std::optional<QColor> color() const {
-		return _paper.backgroundColor();
-	}
+	[[nodiscard]] std::optional<QColor> colorForFill() const;
 	[[nodiscard]] QImage createCurrentImage() const;
 	[[nodiscard]] bool tile() const;
 	[[nodiscard]] bool tileDay() const;