storage_media_prepare.cpp 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. /*
  2. This file is part of Telegram Desktop,
  3. the official desktop application for the Telegram messaging service.
  4. For license and copyright information please follow this link:
  5. https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
  6. */
  7. #include "storage/storage_media_prepare.h"
  8. #include "editor/photo_editor_common.h"
  9. #include "platform/platform_file_utilities.h"
  10. #include "lang/lang_keys.h"
  11. #include "storage/localimageloader.h"
  12. #include "core/mime_type.h"
  13. #include "ui/image/image_prepare.h"
  14. #include "ui/chat/attach/attach_prepare.h"
  15. #include "core/crash_reports.h"
  16. #include <QtCore/QSemaphore>
  17. #include <QtCore/QMimeData>
  18. namespace Storage {
  19. namespace {
  20. using Ui::PreparedFileInformation;
  21. using Ui::PreparedFile;
  22. using Ui::PreparedList;
  23. using Image = PreparedFileInformation::Image;
  24. bool ValidPhotoForAlbum(
  25. const Image &image,
  26. const QString &mime) {
  27. Expects(!image.data.isNull());
  28. if (image.animated
  29. || (!mime.isEmpty() && !mime.startsWith(u"image/"))) {
  30. return false;
  31. }
  32. const auto width = image.data.width();
  33. const auto height = image.data.height();
  34. return Ui::ValidateThumbDimensions(width, height);
  35. }
  36. bool ValidVideoForAlbum(const PreparedFileInformation::Video &video) {
  37. const auto width = video.thumbnail.width();
  38. const auto height = video.thumbnail.height();
  39. return Ui::ValidateThumbDimensions(width, height);
  40. }
  41. QSize PrepareShownDimensions(const QImage &preview, int sideLimit) {
  42. const auto result = preview.size();
  43. return (result.width() > sideLimit || result.height() > sideLimit)
  44. ? result.scaled(sideLimit, sideLimit, Qt::KeepAspectRatio)
  45. : result;
  46. }
  47. void PrepareDetailsInParallel(PreparedList &result, int previewWidth) {
  48. Expects(result.files.size() <= Ui::MaxAlbumItems());
  49. if (result.files.empty()) {
  50. return;
  51. }
  52. const auto sideLimit = PhotoSideLimit(); // Get on main thread.
  53. QSemaphore semaphore;
  54. for (auto &file : result.files) {
  55. crl::async([=, &semaphore, &file] {
  56. PrepareDetails(file, previewWidth, sideLimit);
  57. semaphore.release();
  58. });
  59. }
  60. semaphore.acquire(result.files.size());
  61. }
  62. } // namespace
  63. bool ValidatePhotoEditorMediaDragData(not_null<const QMimeData*> data) {
  64. const auto urls = Core::ReadMimeUrls(data);
  65. if (urls.size() > 1) {
  66. return false;
  67. } else if (data->hasImage()) {
  68. return true;
  69. }
  70. if (!urls.isEmpty()) {
  71. const auto url = urls.front();
  72. if (url.isLocalFile()) {
  73. using namespace Core;
  74. const auto file = Platform::File::UrlToLocal(url);
  75. const auto info = QFileInfo(file);
  76. return FileIsImage(file, MimeTypeForFile(info).name())
  77. && QImageReader(file).canRead();
  78. }
  79. }
  80. return false;
  81. }
  82. bool ValidateEditMediaDragData(
  83. not_null<const QMimeData*> data,
  84. Ui::AlbumType albumType) {
  85. const auto urls = Core::ReadMimeUrls(data);
  86. if (urls.size() > 1) {
  87. return false;
  88. } else if (data->hasImage()) {
  89. return (albumType != Ui::AlbumType::Music);
  90. }
  91. if (albumType == Ui::AlbumType::PhotoVideo && !urls.isEmpty()) {
  92. const auto url = urls.front();
  93. if (url.isLocalFile()) {
  94. using namespace Core;
  95. const auto info = QFileInfo(Platform::File::UrlToLocal(url));
  96. return IsMimeAcceptedForPhotoVideoAlbum(MimeTypeForFile(info).name());
  97. }
  98. }
  99. return true;
  100. }
  101. MimeDataState ComputeMimeDataState(const QMimeData *data) {
  102. if (!data || data->hasFormat(u"application/x-td-forward"_q)) {
  103. return MimeDataState::None;
  104. }
  105. if (data->hasImage()) {
  106. return MimeDataState::Image;
  107. }
  108. const auto urls = Core::ReadMimeUrls(data);
  109. if (urls.isEmpty()) {
  110. return MimeDataState::None;
  111. }
  112. auto allAreSmallImages = true;
  113. for (const auto &url : urls) {
  114. if (!url.isLocalFile()) {
  115. return MimeDataState::None;
  116. }
  117. const auto file = Platform::File::UrlToLocal(url);
  118. const auto info = QFileInfo(file);
  119. if (info.isDir()) {
  120. return MimeDataState::None;
  121. }
  122. using namespace Core;
  123. const auto filesize = info.size();
  124. if (filesize > kFileSizePremiumLimit) {
  125. return MimeDataState::None;
  126. //} else if (filesize > kFileSizeLimit) {
  127. // return MimeDataState::PremiumFile;
  128. } else if (allAreSmallImages) {
  129. if (filesize > Images::kReadBytesLimit) {
  130. allAreSmallImages = false;
  131. } else {
  132. const auto mime = MimeTypeForFile(info).name();
  133. if (mime == u"image/gif"_q
  134. || !FileIsImage(file, mime)
  135. || !QImageReader(file).canRead()) {
  136. allAreSmallImages = false;
  137. }
  138. }
  139. }
  140. }
  141. return allAreSmallImages
  142. ? MimeDataState::PhotoFiles
  143. : MimeDataState::Files;
  144. }
  145. PreparedList PrepareMediaList(
  146. const QList<QUrl> &files,
  147. int previewWidth,
  148. bool premium) {
  149. auto locals = QStringList();
  150. locals.reserve(files.size());
  151. for (const auto &url : files) {
  152. if (!url.isLocalFile()) {
  153. return {
  154. PreparedList::Error::NonLocalUrl,
  155. url.toDisplayString()
  156. };
  157. }
  158. locals.push_back(Platform::File::UrlToLocal(url));
  159. }
  160. return PrepareMediaList(locals, previewWidth, premium);
  161. }
  162. PreparedList PrepareMediaList(
  163. const QStringList &files,
  164. int previewWidth,
  165. bool premium) {
  166. auto result = PreparedList();
  167. result.files.reserve(files.size());
  168. for (const auto &file : files) {
  169. const auto fileinfo = QFileInfo(file);
  170. const auto filesize = fileinfo.size();
  171. if (fileinfo.isDir()) {
  172. return {
  173. PreparedList::Error::Directory,
  174. file
  175. };
  176. } else if (filesize <= 0) {
  177. return {
  178. PreparedList::Error::EmptyFile,
  179. file
  180. };
  181. } else if (filesize > kFileSizePremiumLimit
  182. || (filesize > kFileSizeLimit && !premium)) {
  183. auto errorResult = PreparedList(
  184. PreparedList::Error::TooLargeFile,
  185. QString());
  186. errorResult.files.emplace_back(file);
  187. errorResult.files.back().size = filesize;
  188. return errorResult;
  189. }
  190. if (result.files.size() < Ui::MaxAlbumItems()) {
  191. result.files.emplace_back(file);
  192. result.files.back().size = filesize;
  193. } else {
  194. result.filesToProcess.emplace_back(file);
  195. result.files.back().size = filesize;
  196. }
  197. }
  198. PrepareDetailsInParallel(result, previewWidth);
  199. return result;
  200. }
  201. PreparedList PrepareMediaFromImage(
  202. QImage &&image,
  203. QByteArray &&content,
  204. int previewWidth) {
  205. Expects(!image.isNull());
  206. auto result = PreparedList();
  207. auto file = PreparedFile(QString());
  208. file.content = content;
  209. if (file.content.isEmpty()) {
  210. file.information = std::make_unique<PreparedFileInformation>();
  211. const auto animated = false;
  212. FileLoadTask::FillImageInformation(
  213. std::move(image),
  214. animated,
  215. file.information);
  216. }
  217. result.files.push_back(std::move(file));
  218. PrepareDetailsInParallel(result, previewWidth);
  219. return result;
  220. }
  221. std::optional<PreparedList> PreparedFileFromFilesDialog(
  222. FileDialog::OpenResult &&result,
  223. Fn<bool(const Ui::PreparedList&)> checkResult,
  224. Fn<void(tr::phrase<>)> errorCallback,
  225. int previewWidth,
  226. bool premium) {
  227. if (result.paths.isEmpty() && result.remoteContent.isEmpty()) {
  228. return std::nullopt;
  229. }
  230. auto list = result.remoteContent.isEmpty()
  231. ? PrepareMediaList(result.paths, previewWidth, premium)
  232. : PrepareMediaFromImage(
  233. QImage(),
  234. std::move(result.remoteContent),
  235. previewWidth);
  236. if (list.error != PreparedList::Error::None) {
  237. errorCallback(tr::lng_send_media_invalid_files);
  238. return std::nullopt;
  239. } else if (!checkResult(list)) {
  240. return std::nullopt;
  241. } else {
  242. return list;
  243. }
  244. }
  245. void PrepareDetails(PreparedFile &file, int previewWidth, int sideLimit) {
  246. if (!file.path.isEmpty()) {
  247. file.information = FileLoadTask::ReadMediaInformation(
  248. file.path,
  249. QByteArray(),
  250. Core::MimeTypeForFile(QFileInfo(file.path)).name());
  251. } else if (!file.content.isEmpty()) {
  252. file.information = FileLoadTask::ReadMediaInformation(
  253. QString(),
  254. file.content,
  255. Core::MimeTypeForData(file.content).name());
  256. } else {
  257. Assert(file.information != nullptr);
  258. }
  259. using Video = PreparedFileInformation::Video;
  260. using Song = PreparedFileInformation::Song;
  261. if (const auto image = std::get_if<Image>(
  262. &file.information->media)) {
  263. Assert(!image->data.isNull());
  264. if (ValidPhotoForAlbum(*image, file.information->filemime)) {
  265. UpdateImageDetails(file, previewWidth, sideLimit);
  266. file.type = PreparedFile::Type::Photo;
  267. } else {
  268. file.originalDimensions = image->data.size();
  269. if (image->animated) {
  270. file.type = PreparedFile::Type::None;
  271. }
  272. }
  273. } else if (const auto video = std::get_if<Video>(
  274. &file.information->media)) {
  275. if (ValidVideoForAlbum(*video)) {
  276. auto blurred = Images::Blur(
  277. Images::Opaque(base::duplicate(video->thumbnail)));
  278. file.originalDimensions = video->thumbnail.size();
  279. file.shownDimensions = PrepareShownDimensions(
  280. video->thumbnail,
  281. sideLimit);
  282. file.preview = std::move(blurred).scaledToWidth(
  283. previewWidth * style::DevicePixelRatio(),
  284. Qt::SmoothTransformation);
  285. Assert(!file.preview.isNull());
  286. file.preview.setDevicePixelRatio(style::DevicePixelRatio());
  287. file.type = PreparedFile::Type::Video;
  288. }
  289. } else if (const auto song = std::get_if<Song>(&file.information->media)) {
  290. file.type = PreparedFile::Type::Music;
  291. }
  292. }
  293. void UpdateImageDetails(
  294. PreparedFile &file,
  295. int previewWidth,
  296. int sideLimit) {
  297. const auto image = std::get_if<Image>(&file.information->media);
  298. if (!image) {
  299. return;
  300. }
  301. Assert(!image->data.isNull());
  302. auto preview = image->modifications
  303. ? Editor::ImageModified(image->data, image->modifications)
  304. : image->data;
  305. Assert(!preview.isNull());
  306. file.originalDimensions = preview.size();
  307. file.shownDimensions = PrepareShownDimensions(preview, sideLimit);
  308. const auto toWidth = std::min(
  309. previewWidth,
  310. style::ConvertScale(preview.width())
  311. ) * style::DevicePixelRatio();
  312. auto scaled = preview.scaledToWidth(
  313. toWidth,
  314. Qt::SmoothTransformation);
  315. if (scaled.isNull()) {
  316. CrashReports::SetAnnotation("Info", QString("%1x%2:%3*%4->%5;%6x%7"
  317. ).arg(preview.width()).arg(preview.height()
  318. ).arg(previewWidth).arg(style::DevicePixelRatio()
  319. ).arg(toWidth
  320. ).arg(scaled.width()).arg(scaled.height()));
  321. Unexpected("Scaled is null.");
  322. }
  323. Assert(!scaled.isNull());
  324. file.preview = Images::Opaque(std::move(scaled));
  325. Assert(!file.preview.isNull());
  326. file.preview.setDevicePixelRatio(style::DevicePixelRatio());
  327. }
  328. bool ApplyModifications(PreparedList &list) {
  329. auto applied = false;
  330. const auto apply = [&](PreparedFile &file, QSize strictSize = {}) {
  331. const auto image = std::get_if<Image>(&file.information->media);
  332. const auto guard = gsl::finally([&] {
  333. if (!image || strictSize.isEmpty()) {
  334. return;
  335. }
  336. applied = true;
  337. file.path = QString();
  338. file.content = QByteArray();
  339. image->data = image->data.scaled(
  340. strictSize,
  341. Qt::IgnoreAspectRatio,
  342. Qt::SmoothTransformation);
  343. });
  344. if (!image || !image->modifications) {
  345. return;
  346. }
  347. applied = true;
  348. file.path = QString();
  349. file.content = QByteArray();
  350. image->data = Editor::ImageModified(
  351. std::move(image->data),
  352. image->modifications);
  353. };
  354. for (auto &file : list.files) {
  355. apply(file);
  356. if (const auto cover = file.videoCover.get()) {
  357. const auto video = file.information
  358. ? std::get_if<Ui::PreparedFileInformation::Video>(
  359. &file.information->media)
  360. : nullptr;
  361. apply(*cover, video ? video->thumbnail.size() : QSize());
  362. }
  363. }
  364. return applied;
  365. }
  366. } // namespace Storage