/* This file is part of Telegram Desktop, the official desktop version of Telegram messaging app, see https://telegram.org Telegram Desktop is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. It is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. In addition, as a special exception, the copyright holders give permission to link the code of portions of this program with the OpenSSL library. Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org */ #include "storage/localimageloader.h" #include "core/file_utilities.h" #include "media/media_audio.h" #include "boxes/send_files_box.h" #include "media/media_clip_reader.h" #include "mainwidget.h" #include "mainwindow.h" #include "lang.h" #include "boxes/confirmbox.h" namespace { bool ValidateThumbDimensions(int width, int height) { return (width > 0) && (height > 0) && (width < 20 * height) && (height < 20 * width); } } // namespace TaskQueue::TaskQueue(QObject *parent, int32 stopTimeoutMs) : QObject(parent), _thread(0), _worker(0), _stopTimer(0) { if (stopTimeoutMs > 0) { _stopTimer = new QTimer(this); connect(_stopTimer, SIGNAL(timeout()), this, SLOT(stop())); _stopTimer->setSingleShot(true); _stopTimer->setInterval(stopTimeoutMs); } } TaskId TaskQueue::addTask(TaskPtr task) { { QMutexLocker lock(&_tasksToProcessMutex); _tasksToProcess.push_back(task); } wakeThread(); return task->id(); } void TaskQueue::addTasks(const TasksList &tasks) { { QMutexLocker lock(&_tasksToProcessMutex); _tasksToProcess.append(tasks); } wakeThread(); } void TaskQueue::wakeThread() { if (!_thread) { _thread = new QThread(); _worker = new TaskQueueWorker(this); _worker->moveToThread(_thread); connect(this, SIGNAL(taskAdded()), _worker, SLOT(onTaskAdded())); connect(_worker, SIGNAL(taskProcessed()), this, SLOT(onTaskProcessed())); _thread->start(); } if (_stopTimer) _stopTimer->stop(); emit taskAdded(); } void TaskQueue::cancelTask(TaskId id) { { QMutexLocker lock(&_tasksToProcessMutex); for (int32 i = 0, l = _tasksToProcess.size(); i != l; ++i) { if (_tasksToProcess.at(i)->id() == id) { _tasksToProcess.removeAt(i); return; } } } QMutexLocker lock(&_tasksToFinishMutex); for (int32 i = 0, l = _tasksToFinish.size(); i != l; ++i) { if (_tasksToFinish.at(i)->id() == id) { _tasksToFinish.removeAt(i); return; } } } void TaskQueue::onTaskProcessed() { do { TaskPtr task; { QMutexLocker lock(&_tasksToFinishMutex); if (_tasksToFinish.isEmpty()) break; task = _tasksToFinish.front(); _tasksToFinish.pop_front(); } task->finish(); } while (true); if (_stopTimer) { QMutexLocker lock(&_tasksToProcessMutex); if (_tasksToProcess.isEmpty()) { _stopTimer->start(); } } } void TaskQueue::stop() { if (_thread) { _thread->requestInterruption(); _thread->quit(); DEBUG_LOG(("Waiting for taskThread to finish")); _thread->wait(); delete _worker; delete _thread; _worker = 0; _thread = 0; } _tasksToProcess.clear(); _tasksToFinish.clear(); } TaskQueue::~TaskQueue() { stop(); delete _stopTimer; } void TaskQueueWorker::onTaskAdded() { if (_inTaskAdded) return; _inTaskAdded = true; bool someTasksLeft = false; do { TaskPtr task; { QMutexLocker lock(&_queue->_tasksToProcessMutex); if (!_queue->_tasksToProcess.isEmpty()) { task = _queue->_tasksToProcess.front(); } } if (task) { task->process(); bool emitTaskProcessed = false; { QMutexLocker lockToProcess(&_queue->_tasksToProcessMutex); if (!_queue->_tasksToProcess.isEmpty() && _queue->_tasksToProcess.front() == task) { _queue->_tasksToProcess.pop_front(); someTasksLeft = !_queue->_tasksToProcess.isEmpty(); QMutexLocker lockToFinish(&_queue->_tasksToFinishMutex); emitTaskProcessed = _queue->_tasksToFinish.isEmpty(); _queue->_tasksToFinish.push_back(task); } } if (emitTaskProcessed) { emit taskProcessed(); } } QCoreApplication::processEvents(); } while (someTasksLeft && !thread()->isInterruptionRequested()); _inTaskAdded = false; } FileLoadTask::FileLoadTask(const QString &filepath, std::unique_ptr<MediaInformation> information, SendMediaType type, const FileLoadTo &to, const QString &caption) : _id(rand_value<uint64>()) , _to(to) , _filepath(filepath) , _information(std::move(information)) , _type(type) , _caption(caption) { } FileLoadTask::FileLoadTask(const QByteArray &content, const QImage &image, SendMediaType type, const FileLoadTo &to, const QString &caption) : _id(rand_value<uint64>()) , _to(to) , _content(content) , _image(image) , _type(type) , _caption(caption) { } FileLoadTask::FileLoadTask(const QByteArray &voice, int32 duration, const VoiceWaveform &waveform, const FileLoadTo &to, const QString &caption) : _id(rand_value<uint64>()) , _to(to) , _content(voice) , _duration(duration) , _waveform(waveform) , _type(SendMediaType::Audio) , _caption(caption) { } std::unique_ptr<FileLoadTask::MediaInformation> FileLoadTask::ReadMediaInformation(const QString &filepath, const QByteArray &content, const QString &filemime) { auto result = std::make_unique<MediaInformation>(); result->filemime = filemime; if (CheckForSong(filepath, content, result)) { return result; } else if (CheckForVideo(filepath, content, result)) { return result; } else if (CheckForImage(filepath, content, result)) { return result; } return result; } template <typename Mimes, typename Extensions> bool FileLoadTask::CheckMimeOrExtensions(const QString &filepath, const QString &filemime, Mimes &mimes, Extensions &extensions) { if (std::find(std::begin(mimes), std::end(mimes), filemime) != std::end(mimes)) { return true; } if (std::find_if(std::begin(extensions), std::end(extensions), [&filepath](auto &extension) { return filepath.endsWith(extension, Qt::CaseInsensitive); }) != std::end(extensions)) { return true; } return false; } bool FileLoadTask::CheckForSong(const QString &filepath, const QByteArray &content, std::unique_ptr<MediaInformation> &result) { static const auto mimes = { qstr("audio/mp3"), qstr("audio/m4a"), qstr("audio/aac"), qstr("audio/ogg"), qstr("audio/flac"), }; static const auto extensions = { qstr(".mp3"), qstr(".m4a"), qstr(".aac"), qstr(".ogg"), qstr(".flac"), }; if (!CheckMimeOrExtensions(filepath, result->filemime, mimes, extensions)) { return false; } auto media = Media::Player::PrepareForSending(filepath, content); if (media.duration <= 0) { return false; } if (!ValidateThumbDimensions(media.cover.width(), media.cover.height())) { media.cover = QImage(); } result->media = std::move(media); return true; } bool FileLoadTask::CheckForVideo(const QString &filepath, const QByteArray &content, std::unique_ptr<MediaInformation> &result) { static const auto mimes = { qstr("video/mp4"), qstr("video/quicktime"), }; static const auto extensions = { qstr(".mp4"), qstr(".mov"), }; if (!CheckMimeOrExtensions(filepath, result->filemime, mimes, extensions)) { return false; } auto media = Media::Clip::PrepareForSending(filepath, content); if (media.duration <= 0) { return false; } auto coverWidth = media.thumbnail.width(); auto coverHeight = media.thumbnail.height(); if (!ValidateThumbDimensions(coverWidth, coverHeight)) { return false; } if (filepath.endsWith(qstr(".mp4"), Qt::CaseInsensitive)) { result->filemime = qstr("video/mp4"); } result->media = std::move(media); return true; } bool FileLoadTask::CheckForImage(const QString &filepath, const QByteArray &content, std::unique_ptr<MediaInformation> &result) { auto animated = false; auto image = ([&filepath, &content, &animated] { if (!content.isEmpty()) { return App::readImage(content, nullptr, false, &animated); } else if (!filepath.isEmpty()) { return App::readImage(filepath, nullptr, false, &animated); } return QImage(); })(); if (image.isNull()) { return false; } auto media = Image(); media.data = std::move(image); media.animated = animated; result->media = media; return true; } void FileLoadTask::process() { const auto stickerMime = qsl("image/webp"); _result = MakeShared<FileLoadResult>(_id, _to, _caption); QString filename, filemime; qint64 filesize = 0; QByteArray filedata; uint64 thumbId = 0; auto thumbname = qsl("thumb.jpg"); QByteArray thumbdata; auto isAnimation = false; auto isSong = false; auto isVideo = false; auto isVoice = (_type == SendMediaType::Audio); auto fullimage = base::take(_image); auto info = _filepath.isEmpty() ? QFileInfo() : QFileInfo(_filepath); if (info.exists()) { if (info.isDir()) { _result->filesize = -1; return; } // Voice sending is supported only from memory for now. // Because for voice we force mime type and don't read MediaInformation. // For a real file we always read mime type and read MediaInformation. t_assert(!isVoice); filesize = info.size(); filename = info.fileName(); if (!_information) { _information = readMediaInformation(mimeTypeForFile(info).name()); } filemime = _information->filemime; if (auto image = base::get_if<FileLoadTask::Image>(&_information->media)) { fullimage = base::take(image->data); if (auto opaque = (filemime != stickerMime)) { fullimage = Images::prepareOpaque(std::move(fullimage)); } isAnimation = image->animated; } } else if (!_content.isEmpty()) { filesize = _content.size(); if (isVoice) { filename = filedialogDefaultName(qsl("audio"), qsl(".ogg"), QString(), true); filemime = "audio/ogg"; } else { auto mimeType = mimeTypeForData(_content); filemime = mimeType.name(); if (filemime != stickerMime) { fullimage = Images::prepareOpaque(std::move(fullimage)); } if (filemime == "image/jpeg") { filename = filedialogDefaultName(qsl("photo"), qsl(".jpg"), QString(), true); } else if (filemime == "image/png") { filename = filedialogDefaultName(qsl("image"), qsl(".png"), QString(), true); } else { QString ext; QStringList patterns = mimeType.globPatterns(); if (!patterns.isEmpty()) { ext = patterns.front().replace('*', QString()); } filename = filedialogDefaultName(qsl("file"), ext, QString(), true); } } } else if (!fullimage.isNull() && fullimage.width() > 0) { if (_type == SendMediaType::Photo) { if (ValidateThumbDimensions(fullimage.width(), fullimage.height())) { filesize = -1; // Fill later. filemime = mimeTypeForName("image/jpeg").name(); filename = filedialogDefaultName(qsl("image"), qsl(".jpg"), QString(), true); } else { _type = SendMediaType::File; } } if (_type == SendMediaType::File) { filemime = mimeTypeForName("image/png").name(); filename = filedialogDefaultName(qsl("image"), qsl(".png"), QString(), true); { QBuffer buffer(&_content); fullimage.save(&buffer, "PNG"); } filesize = _content.size(); } fullimage = Images::prepareOpaque(std::move(fullimage)); } _result->filesize = (int32)qMin(filesize, qint64(INT_MAX)); if (!filesize || filesize > App::kFileSizeLimit) { return; } PreparedPhotoThumbs photoThumbs; QVector<MTPPhotoSize> photoSizes; QPixmap thumb; QVector<MTPDocumentAttribute> attributes(1, MTP_documentAttributeFilename(MTP_string(filename))); MTPPhotoSize thumbSize(MTP_photoSizeEmpty(MTP_string(""))); MTPPhoto photo(MTP_photoEmpty(MTP_long(0))); MTPDocument document(MTP_documentEmpty(MTP_long(0))); if (!isVoice) { if (!_information) { _information = readMediaInformation(filemime); filemime = _information->filemime; } if (auto song = base::get_if<Song>(&_information->media)) { isSong = true; auto flags = MTPDdocumentAttributeAudio::Flag::f_title | MTPDdocumentAttributeAudio::Flag::f_performer; attributes.push_back(MTP_documentAttributeAudio(MTP_flags(flags), MTP_int(song->duration), MTP_string(song->title), MTP_string(song->performer), MTPstring())); if (!song->cover.isNull()) { // cover to thumb auto coverWidth = song->cover.width(); auto coverHeight = song->cover.height(); auto full = (coverWidth > 90 || coverHeight > 90) ? App::pixmapFromImageInPlace(song->cover.scaled(90, 90, Qt::KeepAspectRatio, Qt::SmoothTransformation)) : App::pixmapFromImageInPlace(std::move(song->cover)); { auto thumbFormat = QByteArray("JPG"); auto thumbQuality = 87; QBuffer buffer(&thumbdata); full.save(&buffer, thumbFormat, thumbQuality); } thumb = full; thumbSize = MTP_photoSize(MTP_string(""), MTP_fileLocationUnavailable(MTP_long(0), MTP_int(0), MTP_long(0)), MTP_int(full.width()), MTP_int(full.height()), MTP_int(0)); thumbId = rand_value<uint64>(); } } else if (auto video = base::get_if<Video>(&_information->media)) { isVideo = true; auto coverWidth = video->thumbnail.width(); auto coverHeight = video->thumbnail.height(); if (video->isGifv) { attributes.push_back(MTP_documentAttributeAnimated()); } attributes.push_back(MTP_documentAttributeVideo(MTP_int(video->duration), MTP_int(coverWidth), MTP_int(coverHeight))); auto cover = (coverWidth > 90 || coverHeight > 90) ? video->thumbnail.scaled(90, 90, Qt::KeepAspectRatio, Qt::SmoothTransformation) : std::move(video->thumbnail); { auto thumbFormat = QByteArray("JPG"); auto thumbQuality = 87; QBuffer buffer(&thumbdata); cover.save(&buffer, thumbFormat, thumbQuality); } thumb = App::pixmapFromImageInPlace(std::move(cover)); thumbSize = MTP_photoSize(MTP_string(""), MTP_fileLocationUnavailable(MTP_long(0), MTP_int(0), MTP_long(0)), MTP_int(thumb.width()), MTP_int(thumb.height()), MTP_int(0)); thumbId = rand_value<uint64>(); } } if (!fullimage.isNull() && fullimage.width() > 0 && !isSong && !isVideo && !isVoice) { auto w = fullimage.width(), h = fullimage.height(); attributes.push_back(MTP_documentAttributeImageSize(MTP_int(w), MTP_int(h))); if (ValidateThumbDimensions(w, h)) { if (isAnimation) { attributes.push_back(MTP_documentAttributeAnimated()); } else if (_type != SendMediaType::File) { auto thumb = (w > 100 || h > 100) ? App::pixmapFromImageInPlace(fullimage.scaled(100, 100, Qt::KeepAspectRatio, Qt::SmoothTransformation)) : QPixmap::fromImage(fullimage); photoThumbs.insert('s', thumb); photoSizes.push_back(MTP_photoSize(MTP_string("s"), MTP_fileLocationUnavailable(MTP_long(0), MTP_int(0), MTP_long(0)), MTP_int(thumb.width()), MTP_int(thumb.height()), MTP_int(0))); auto medium = (w > 320 || h > 320) ? App::pixmapFromImageInPlace(fullimage.scaled(320, 320, Qt::KeepAspectRatio, Qt::SmoothTransformation)) : QPixmap::fromImage(fullimage); photoThumbs.insert('m', medium); photoSizes.push_back(MTP_photoSize(MTP_string("m"), MTP_fileLocationUnavailable(MTP_long(0), MTP_int(0), MTP_long(0)), MTP_int(medium.width()), MTP_int(medium.height()), MTP_int(0))); auto full = (w > 1280 || h > 1280) ? App::pixmapFromImageInPlace(fullimage.scaled(1280, 1280, Qt::KeepAspectRatio, Qt::SmoothTransformation)) : QPixmap::fromImage(fullimage); photoThumbs.insert('y', full); photoSizes.push_back(MTP_photoSize(MTP_string("y"), MTP_fileLocationUnavailable(MTP_long(0), MTP_int(0), MTP_long(0)), MTP_int(full.width()), MTP_int(full.height()), MTP_int(0))); { QBuffer buffer(&filedata); full.save(&buffer, "JPG", 87); } MTPDphoto::Flags photoFlags = 0; photo = MTP_photo(MTP_flags(photoFlags), MTP_long(_id), MTP_long(0), MTP_int(unixtime()), MTP_vector<MTPPhotoSize>(photoSizes)); if (filesize < 0) { filesize = _result->filesize = filedata.size(); } } QByteArray thumbFormat = "JPG"; int32 thumbQuality = 87; if (!isAnimation && filemime == stickerMime && w > 0 && h > 0 && w <= StickerMaxSize && h <= StickerMaxSize && filesize < StickerInMemory) { MTPDdocumentAttributeSticker::Flags stickerFlags = 0; attributes.push_back(MTP_documentAttributeSticker(MTP_flags(stickerFlags), MTP_string(""), MTP_inputStickerSetEmpty(), MTPMaskCoords())); thumbFormat = "webp"; thumbname = qsl("thumb.webp"); } QPixmap full = (w > 90 || h > 90) ? App::pixmapFromImageInPlace(fullimage.scaled(90, 90, Qt::KeepAspectRatio, Qt::SmoothTransformation)) : QPixmap::fromImage(fullimage, Qt::ColorOnly); { QBuffer buffer(&thumbdata); full.save(&buffer, thumbFormat, thumbQuality); } thumb = full; thumbSize = MTP_photoSize(MTP_string(""), MTP_fileLocationUnavailable(MTP_long(0), MTP_int(0), MTP_long(0)), MTP_int(full.width()), MTP_int(full.height()), MTP_int(0)); thumbId = rand_value<uint64>(); } } if (isVoice) { attributes[0] = MTP_documentAttributeAudio(MTP_flags(MTPDdocumentAttributeAudio::Flag::f_voice | MTPDdocumentAttributeAudio::Flag::f_waveform), MTP_int(_duration), MTPstring(), MTPstring(), MTP_bytes(documentWaveformEncode5bit(_waveform))); attributes.resize(1); document = MTP_document(MTP_long(_id), MTP_long(0), MTP_int(unixtime()), MTP_string(filemime), MTP_int(filesize), thumbSize, MTP_int(MTP::maindc()), MTP_int(0), MTP_vector<MTPDocumentAttribute>(attributes)); } else if (_type != SendMediaType::Photo) { document = MTP_document(MTP_long(_id), MTP_long(0), MTP_int(unixtime()), MTP_string(filemime), MTP_int(filesize), thumbSize, MTP_int(MTP::maindc()), MTP_int(0), MTP_vector<MTPDocumentAttribute>(attributes)); _type = SendMediaType::File; } _result->type = _type; _result->filepath = _filepath; _result->content = _content; _result->filename = filename; _result->filemime = filemime; _result->setFileData(filedata); _result->thumbId = thumbId; _result->thumbname = thumbname; _result->setThumbData(thumbdata); _result->thumb = thumb; _result->photo = photo; _result->document = document; _result->photoThumbs = photoThumbs; } void FileLoadTask::finish() { if (!_result || !_result->filesize) { Ui::show(Box<InformBox>(lng_send_image_empty(lt_name, _filepath)), KeepOtherLayers); } else if (_result->filesize == -1) { // dir Ui::show(Box<InformBox>(lng_send_folder(lt_name, QFileInfo(_filepath).dir().dirName())), KeepOtherLayers); } else if (_result->filesize > App::kFileSizeLimit) { Ui::show(Box<InformBox>(lng_send_image_too_large(lt_name, _filepath)), KeepOtherLayers); } else if (App::main()) { App::main()->onSendFileConfirm(_result); } }