414 lines
11 KiB
C++
414 lines
11 KiB
C++
/*
|
|
This file is part of Telegram Desktop,
|
|
the official desktop application for the Telegram messaging service.
|
|
|
|
For license and copyright information please follow this link:
|
|
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|
*/
|
|
#include "storage/storage_media_prepare.h"
|
|
|
|
#include "platform/platform_file_utilities.h"
|
|
#include "storage/localimageloader.h"
|
|
#include "core/mime_type.h"
|
|
#include "ui/image/image_prepare.h"
|
|
#include "app.h"
|
|
|
|
#include <QtCore/QSemaphore>
|
|
#include <QtCore/QMimeData>
|
|
|
|
namespace Storage {
|
|
namespace {
|
|
|
|
constexpr auto kMaxAlbumCount = 10;
|
|
|
|
bool HasExtensionFrom(const QString &file, const QStringList &extensions) {
|
|
for (const auto &extension : extensions) {
|
|
const auto ext = file.right(extension.size());
|
|
if (ext.compare(extension, Qt::CaseInsensitive) == 0) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool ValidPhotoForAlbum(
|
|
const FileMediaInformation::Image &image,
|
|
const QString &mime) {
|
|
if (image.animated
|
|
|| mime == qstr("image/webp")
|
|
|| mime == qstr("application/x-tgsticker")) {
|
|
return false;
|
|
}
|
|
const auto width = image.data.width();
|
|
const auto height = image.data.height();
|
|
return ValidateThumbDimensions(width, height);
|
|
}
|
|
|
|
bool ValidVideoForAlbum(const FileMediaInformation::Video &video) {
|
|
const auto width = video.thumbnail.width();
|
|
const auto height = video.thumbnail.height();
|
|
return ValidateThumbDimensions(width, height);
|
|
}
|
|
|
|
QSize PrepareShownDimensions(const QImage &preview) {
|
|
constexpr auto kMaxWidth = 1280;
|
|
constexpr auto kMaxHeight = 1280;
|
|
|
|
const auto result = preview.size();
|
|
return (result.width() > kMaxWidth || result.height() > kMaxHeight)
|
|
? result.scaled(kMaxWidth, kMaxHeight, Qt::KeepAspectRatio)
|
|
: result;
|
|
}
|
|
|
|
bool PrepareAlbumMediaIsWaiting(
|
|
QSemaphore &semaphore,
|
|
PreparedFile &file,
|
|
int previewWidth) {
|
|
// TODO: Use some special thread queue, like a separate QThreadPool.
|
|
crl::async([=, &semaphore, &file] {
|
|
const auto guard = gsl::finally([&] { semaphore.release(); });
|
|
if (!file.path.isEmpty()) {
|
|
file.mime = Core::MimeTypeForFile(QFileInfo(file.path)).name();
|
|
file.information = FileLoadTask::ReadMediaInformation(
|
|
file.path,
|
|
QByteArray(),
|
|
file.mime);
|
|
} else if (!file.content.isEmpty()) {
|
|
file.mime = Core::MimeTypeForData(file.content).name();
|
|
file.information = FileLoadTask::ReadMediaInformation(
|
|
QString(),
|
|
file.content,
|
|
file.mime);
|
|
} else {
|
|
Assert(file.information != nullptr);
|
|
}
|
|
|
|
using Image = FileMediaInformation::Image;
|
|
using Video = FileMediaInformation::Video;
|
|
if (const auto image = base::get_if<Image>(
|
|
&file.information->media)) {
|
|
if (ValidPhotoForAlbum(*image, file.mime)) {
|
|
file.shownDimensions = PrepareShownDimensions(image->data);
|
|
file.preview = Images::prepareOpaque(image->data.scaledToWidth(
|
|
std::min(previewWidth, style::ConvertScale(image->data.width()))
|
|
* cIntRetinaFactor(),
|
|
Qt::SmoothTransformation));
|
|
Assert(!file.preview.isNull());
|
|
file.preview.setDevicePixelRatio(cRetinaFactor());
|
|
file.type = PreparedFile::AlbumType::Photo;
|
|
}
|
|
} else if (const auto video = base::get_if<Video>(
|
|
&file.information->media)) {
|
|
if (ValidVideoForAlbum(*video)) {
|
|
auto blurred = Images::prepareBlur(Images::prepareOpaque(video->thumbnail));
|
|
file.shownDimensions = PrepareShownDimensions(video->thumbnail);
|
|
file.preview = std::move(blurred).scaledToWidth(
|
|
previewWidth * cIntRetinaFactor(),
|
|
Qt::SmoothTransformation);
|
|
Assert(!file.preview.isNull());
|
|
file.preview.setDevicePixelRatio(cRetinaFactor());
|
|
file.type = PreparedFile::AlbumType::Video;
|
|
}
|
|
}
|
|
});
|
|
return true;
|
|
}
|
|
|
|
void PrepareAlbum(PreparedList &result, int previewWidth) {
|
|
const auto count = int(result.files.size());
|
|
if (count > kMaxAlbumCount) {
|
|
return;
|
|
}
|
|
|
|
result.albumIsPossible = (count > 1);
|
|
auto waiting = 0;
|
|
QSemaphore semaphore;
|
|
for (auto &file : result.files) {
|
|
if (PrepareAlbumMediaIsWaiting(semaphore, file, previewWidth)) {
|
|
++waiting;
|
|
}
|
|
}
|
|
if (waiting > 0) {
|
|
semaphore.acquire(waiting);
|
|
if (result.albumIsPossible) {
|
|
const auto badIt = ranges::find(
|
|
result.files,
|
|
PreparedFile::AlbumType::None,
|
|
[](const PreparedFile &file) { return file.type; });
|
|
result.albumIsPossible = (badIt == result.files.end());
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool ValidateThumbDimensions(int width, int height) {
|
|
return (width > 0)
|
|
&& (height > 0)
|
|
&& (width < 20 * height)
|
|
&& (height < 20 * width);
|
|
}
|
|
|
|
PreparedFile::PreparedFile(const QString &path) : path(path) {
|
|
}
|
|
|
|
PreparedFile::PreparedFile(PreparedFile &&other) = default;
|
|
|
|
PreparedFile &PreparedFile::operator=(PreparedFile &&other) = default;
|
|
|
|
PreparedFile::~PreparedFile() = default;
|
|
|
|
MimeDataState ComputeMimeDataState(const QMimeData *data) {
|
|
if (!data || data->hasFormat(qsl("application/x-td-forward"))) {
|
|
return MimeDataState::None;
|
|
}
|
|
|
|
if (data->hasImage()) {
|
|
return MimeDataState::Image;
|
|
}
|
|
|
|
const auto uriListFormat = qsl("text/uri-list");
|
|
if (!data->hasFormat(uriListFormat)) {
|
|
return MimeDataState::None;
|
|
}
|
|
|
|
const auto &urls = data->urls();
|
|
if (urls.isEmpty()) {
|
|
return MimeDataState::None;
|
|
}
|
|
|
|
const auto imageExtensions = cImgExtensions();
|
|
auto files = QStringList();
|
|
auto allAreSmallImages = true;
|
|
for (const auto &url : urls) {
|
|
if (!url.isLocalFile()) {
|
|
return MimeDataState::None;
|
|
}
|
|
const auto file = Platform::File::UrlToLocal(url);
|
|
|
|
const auto info = QFileInfo(file);
|
|
if (info.isDir()) {
|
|
return MimeDataState::None;
|
|
}
|
|
|
|
const auto filesize = info.size();
|
|
if (filesize > App::kFileSizeLimit) {
|
|
return MimeDataState::None;
|
|
} else if (allAreSmallImages) {
|
|
if (filesize > App::kImageSizeLimit) {
|
|
allAreSmallImages = false;
|
|
} else if (!HasExtensionFrom(file, imageExtensions)) {
|
|
allAreSmallImages = false;
|
|
}
|
|
}
|
|
}
|
|
return allAreSmallImages
|
|
? MimeDataState::PhotoFiles
|
|
: MimeDataState::Files;
|
|
}
|
|
|
|
PreparedList PrepareMediaList(const QList<QUrl> &files, int previewWidth) {
|
|
auto locals = QStringList();
|
|
locals.reserve(files.size());
|
|
for (const auto &url : files) {
|
|
if (!url.isLocalFile()) {
|
|
return {
|
|
PreparedList::Error::NonLocalUrl,
|
|
url.toDisplayString()
|
|
};
|
|
}
|
|
locals.push_back(Platform::File::UrlToLocal(url));
|
|
}
|
|
return PrepareMediaList(locals, previewWidth);
|
|
}
|
|
|
|
PreparedList PrepareMediaList(const QStringList &files, int previewWidth) {
|
|
auto result = PreparedList();
|
|
result.files.reserve(files.size());
|
|
const auto extensionsToCompress = cExtensionsForCompress();
|
|
for (const auto &file : files) {
|
|
const auto fileinfo = QFileInfo(file);
|
|
const auto filesize = fileinfo.size();
|
|
if (fileinfo.isDir()) {
|
|
return {
|
|
PreparedList::Error::Directory,
|
|
file
|
|
};
|
|
} else if (filesize <= 0) {
|
|
return {
|
|
PreparedList::Error::EmptyFile,
|
|
file
|
|
};
|
|
} else if (filesize > App::kFileSizeLimit) {
|
|
return {
|
|
PreparedList::Error::TooLargeFile,
|
|
file
|
|
};
|
|
}
|
|
const auto toCompress = HasExtensionFrom(file, extensionsToCompress);
|
|
if (filesize > App::kImageSizeLimit || !toCompress) {
|
|
result.allFilesForCompress = false;
|
|
}
|
|
result.files.emplace_back(file);
|
|
}
|
|
PrepareAlbum(result, previewWidth);
|
|
return result;
|
|
}
|
|
|
|
PreparedList PrepareMediaFromImage(
|
|
QImage &&image,
|
|
QByteArray &&content,
|
|
int previewWidth) {
|
|
auto result = Storage::PreparedList();
|
|
result.allFilesForCompress = ValidateThumbDimensions(
|
|
image.width(),
|
|
image.height());
|
|
auto file = PreparedFile(QString());
|
|
file.content = content;
|
|
if (file.content.isEmpty()) {
|
|
file.information = std::make_unique<FileMediaInformation>();
|
|
const auto animated = false;
|
|
FileLoadTask::FillImageInformation(
|
|
std::move(image),
|
|
animated,
|
|
file.information);
|
|
}
|
|
result.files.push_back(std::move(file));
|
|
PrepareAlbum(result, previewWidth);
|
|
return result;
|
|
}
|
|
|
|
std::optional<PreparedList> PreparedList::EditedPreparedFile(
|
|
FileDialog::OpenResult &&result,
|
|
bool isAlbum,
|
|
Fn<void()> errorCallback,
|
|
Fn<bool(QString)> isValidFileCallback,
|
|
int previewWidth) {
|
|
if (result.paths.isEmpty() && result.remoteContent.isEmpty()) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
if (!result.remoteContent.isEmpty()) {
|
|
|
|
auto list = Storage::PrepareMediaFromImage(
|
|
QImage(),
|
|
std::move(result.remoteContent),
|
|
previewWidth);
|
|
|
|
if (!isValidFileCallback(list.files.front().mime)) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
if (isAlbum) {
|
|
const auto albumMimes = {
|
|
"image/jpeg",
|
|
"image/png",
|
|
"video/mp4",
|
|
};
|
|
const auto file = &list.files.front();
|
|
if (ranges::find(albumMimes, file->mime) == end(albumMimes)
|
|
|| file->type == Storage::PreparedFile::AlbumType::None) {
|
|
errorCallback();
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
Expects(list.files.size() == 1);
|
|
return std::move(list);
|
|
} else if (!result.paths.isEmpty()) {
|
|
auto list = Storage::PrepareMediaList(
|
|
QStringList(result.paths.front()),
|
|
previewWidth);
|
|
|
|
// Don't rewrite _preparedList if a new list is not valid for album.
|
|
if (isAlbum) {
|
|
using Info = FileMediaInformation;
|
|
|
|
const auto media = &list.files.front().information->media;
|
|
const auto valid = media->match([&](const Info::Image &data) {
|
|
return Storage::ValidateThumbDimensions(
|
|
data.data.width(),
|
|
data.data.height())
|
|
&& !data.animated;
|
|
}, [&](Info::Video &data) {
|
|
data.isGifv = false;
|
|
return true;
|
|
}, [](auto &&other) {
|
|
return false;
|
|
});
|
|
if (!valid) {
|
|
errorCallback();
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
const auto info = QFileInfo(result.paths.front());
|
|
if (!isValidFileCallback(Core::MimeTypeForFile(info).name())) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
Expects(list.files.size() == 1);
|
|
return std::move(list);
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
PreparedList PreparedList::Reordered(
|
|
PreparedList &&list,
|
|
std::vector<int> order) {
|
|
Expects(list.error == PreparedList::Error::None);
|
|
Expects(list.files.size() == order.size());
|
|
|
|
auto result = PreparedList(list.error, list.errorData);
|
|
result.albumIsPossible = list.albumIsPossible;
|
|
result.allFilesForCompress = list.allFilesForCompress;
|
|
result.files.reserve(list.files.size());
|
|
for (auto index : order) {
|
|
result.files.push_back(std::move(list.files[index]));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void PreparedList::mergeToEnd(PreparedList &&other) {
|
|
if (error != Error::None) {
|
|
return;
|
|
}
|
|
if (other.error != Error::None) {
|
|
error = other.error;
|
|
errorData = other.errorData;
|
|
return;
|
|
}
|
|
allFilesForCompress = allFilesForCompress && other.allFilesForCompress;
|
|
files.reserve(files.size() + other.files.size());
|
|
for (auto &file : other.files) {
|
|
files.push_back(std::move(file));
|
|
}
|
|
if (files.size() > 1 && files.size() <= kMaxAlbumCount) {
|
|
const auto badIt = ranges::find(
|
|
files,
|
|
PreparedFile::AlbumType::None,
|
|
[](const PreparedFile &file) { return file.type; });
|
|
albumIsPossible = (badIt == files.end());
|
|
} else {
|
|
albumIsPossible = false;
|
|
}
|
|
}
|
|
|
|
bool PreparedList::canAddCaption(bool isAlbum, bool compressImages) const {
|
|
const auto isSticker = [&] {
|
|
if (files.empty()) {
|
|
return false;
|
|
}
|
|
return (files.front().mime == qstr("image/webp"))
|
|
|| files.front().path.endsWith(
|
|
qstr(".tgs"),
|
|
Qt::CaseInsensitive);
|
|
};
|
|
return isAlbum || (files.size() == 1 && !isSticker());
|
|
}
|
|
|
|
int MaxAlbumItems() {
|
|
return kMaxAlbumCount;
|
|
}
|
|
|
|
} // namespace Storage
|
|
|