Don't recompress some JPEGs when sending as photos.

If JPEG is saved in progressive mode and has bpp <= 4
and max(width, height) <= 1280 then we send original bytes.
This commit is contained in:
John Preston 2022-03-09 17:37:48 +04:00
parent e84ebc2a5c
commit 6805259f74
4 changed files with 77 additions and 23 deletions

View file

@ -25,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history_item.h" #include "history/history_item.h"
#include "boxes/send_files_box.h" #include "boxes/send_files_box.h"
#include "ui/boxes/confirm_box.h" #include "ui/boxes/confirm_box.h"
#include "ui/image/image_prepare.h"
#include "lang/lang_keys.h" #include "lang/lang_keys.h"
#include "storage/file_download.h" #include "storage/file_download.h"
#include "storage/storage_media_prepare.h" #include "storage/storage_media_prepare.h"
@ -42,6 +43,7 @@ namespace {
constexpr auto kThumbnailQuality = 87; constexpr auto kThumbnailQuality = 87;
constexpr auto kThumbnailSize = 320; constexpr auto kThumbnailSize = 320;
constexpr auto kPhotoUploadPartSize = 32 * 1024; constexpr auto kPhotoUploadPartSize = 32 * 1024;
constexpr auto kRecompressAfterBpp = 4;
using Ui::ValidateThumbDimensions; using Ui::ValidateThumbDimensions;
@ -53,7 +55,7 @@ struct PreparedFileThumbnail {
MTPPhotoSize mtpSize = MTP_photoSizeEmpty(MTP_string()); MTPPhotoSize mtpSize = MTP_photoSizeEmpty(MTP_string());
}; };
PreparedFileThumbnail PrepareFileThumbnail(QImage &&original) { [[nodiscard]] PreparedFileThumbnail PrepareFileThumbnail(QImage &&original) {
const auto width = original.width(); const auto width = original.width();
const auto height = original.height(); const auto height = original.height();
if (!ValidateThumbDimensions(width, height)) { if (!ValidateThumbDimensions(width, height)) {
@ -87,7 +89,9 @@ PreparedFileThumbnail PrepareFileThumbnail(QImage &&original) {
return result; return result;
} }
bool FileThumbnailUploadRequired(const QString &filemime, int32 filesize) { [[nodiscard]] bool FileThumbnailUploadRequired(
const QString &filemime,
int32 filesize) {
constexpr auto kThumbnailUploadBySize = 5 * 1024 * 1024; constexpr auto kThumbnailUploadBySize = 5 * 1024 * 1024;
const auto kThumbnailKnownMimes = { const auto kThumbnailKnownMimes = {
"image/jpeg", "image/jpeg",
@ -101,7 +105,7 @@ bool FileThumbnailUploadRequired(const QString &filemime, int32 filesize) {
== end(kThumbnailKnownMimes)); == end(kThumbnailKnownMimes));
} }
PreparedFileThumbnail FinalizeFileThumbnail( [[nodiscard]] PreparedFileThumbnail FinalizeFileThumbnail(
PreparedFileThumbnail &&prepared, PreparedFileThumbnail &&prepared,
const QString &filemime, const QString &filemime,
int32 filesize, int32 filesize,
@ -115,7 +119,7 @@ PreparedFileThumbnail FinalizeFileThumbnail(
return std::move(prepared); return std::move(prepared);
} }
auto FindAlbumItem( [[nodiscard]] auto FindAlbumItem(
std::vector<SendingAlbum::Item> &items, std::vector<SendingAlbum::Item> &items,
not_null<HistoryItem*> item) { not_null<HistoryItem*> item) {
const auto result = ranges::find( const auto result = ranges::find(
@ -127,7 +131,7 @@ auto FindAlbumItem(
return result; return result;
} }
MTPInputSingleMedia PrepareAlbumItemMedia( [[nodiscard]] MTPInputSingleMedia PrepareAlbumItemMedia(
not_null<HistoryItem*> item, not_null<HistoryItem*> item,
const MTPInputMedia &media, const MTPInputMedia &media,
uint64 randomId) { uint64 randomId) {
@ -149,7 +153,7 @@ MTPInputSingleMedia PrepareAlbumItemMedia(
sentEntities); sentEntities);
} }
std::vector<not_null<DocumentData*>> ExtractStickersFromScene( [[nodiscard]] std::vector<not_null<DocumentData*>> ExtractStickersFromScene(
not_null<const Ui::PreparedFileInformation::Image*> info) { not_null<const Ui::PreparedFileInformation::Image*> info) {
const auto allItems = info->modifications.paint->items(); const auto allItems = info->modifications.paint->items();
@ -162,6 +166,33 @@ std::vector<not_null<DocumentData*>> ExtractStickersFromScene(
}) | ranges::to_vector; }) | ranges::to_vector;
} }
[[nodiscard]] QByteArray ComputePhotoJpegBytes(
QImage &full,
const QByteArray &bytes,
const QByteArray &format) {
if (!bytes.isEmpty()
&& (bytes.size()
<= full.width() * full.height() * kRecompressAfterBpp / 8)
&& (format == u"jpeg"_q)
&& Images::IsProgressiveJpeg(bytes)) {
return bytes;
}
// We have an example of dark .png image that when being sent without
// removing its color space is displayed fine on tdesktop, but with
// a light gray background on mobile apps.
full.setColorSpace(QColorSpace());
auto result = QByteArray();
QBuffer buffer(&result);
QImageWriter writer(&buffer, "JPEG");
writer.setQuality(87);
writer.setProgressiveScanWrite(true);
writer.write(full);
buffer.close();
return result;
}
} // namespace } // namespace
SendMediaPrepare::SendMediaPrepare( SendMediaPrepare::SendMediaPrepare(
@ -663,15 +694,23 @@ bool FileLoadTask::CheckForImage(
return Images::Read({ return Images::Read({
.path = filepath, .path = filepath,
.content = content, .content = content,
.returnContent = true,
}); });
}(); }();
return FillImageInformation(std::move(read.image), read.animated, result); return FillImageInformation(
std::move(read.image),
read.animated,
result,
std::move(read.content),
std::move(read.format));
} }
bool FileLoadTask::FillImageInformation( bool FileLoadTask::FillImageInformation(
QImage &&image, QImage &&image,
bool animated, bool animated,
std::unique_ptr<Ui::PreparedFileInformation> &result) { std::unique_ptr<Ui::PreparedFileInformation> &result,
QByteArray content,
QByteArray format) {
Expects(result != nullptr); Expects(result != nullptr);
if (image.isNull()) { if (image.isNull()) {
@ -679,6 +718,8 @@ bool FileLoadTask::FillImageInformation(
} }
auto media = Ui::PreparedFileInformation::Image(); auto media = Ui::PreparedFileInformation::Image();
media.data = std::move(image); media.data = std::move(image);
media.bytes = std::move(content);
media.format = std::move(format);
media.animated = animated; media.animated = animated;
result->media = media; result->media = media;
return true; return true;
@ -703,6 +744,8 @@ void FileLoadTask::process(Args &&args) {
auto isSticker = false; auto isSticker = false;
auto fullimage = QImage(); auto fullimage = QImage();
auto fullimagebytes = QByteArray();
auto fullimageformat = QByteArray();
auto info = _filepath.isEmpty() ? QFileInfo() : QFileInfo(_filepath); auto info = _filepath.isEmpty() ? QFileInfo() : QFileInfo(_filepath);
if (info.exists()) { if (info.exists()) {
if (info.isDir()) { if (info.isDir()) {
@ -724,8 +767,12 @@ void FileLoadTask::process(Args &&args) {
if (auto image = std::get_if<Ui::PreparedFileInformation::Image>( if (auto image = std::get_if<Ui::PreparedFileInformation::Image>(
&_information->media)) { &_information->media)) {
fullimage = base::take(image->data); fullimage = base::take(image->data);
if (!Core::IsMimeSticker(filemime)) { fullimagebytes = base::take(image->bytes);
fullimageformat = base::take(image->format);
if (!Core::IsMimeSticker(filemime)
&& fullimageformat != u"jpeg"_q) {
fullimage = Images::Opaque(std::move(fullimage)); fullimage = Images::Opaque(std::move(fullimage));
fullimagebytes = fullimageformat = QByteArray();
} }
isAnimation = image->animated; isAnimation = image->animated;
} }
@ -739,12 +786,16 @@ void FileLoadTask::process(Args &&args) {
if (auto image = std::get_if<Ui::PreparedFileInformation::Image>( if (auto image = std::get_if<Ui::PreparedFileInformation::Image>(
&_information->media)) { &_information->media)) {
fullimage = base::take(image->data); fullimage = base::take(image->data);
fullimagebytes = base::take(image->bytes);
fullimageformat = base::take(image->format);
} }
} }
const auto mimeType = Core::MimeTypeForData(_content); const auto mimeType = Core::MimeTypeForData(_content);
filemime = mimeType.name(); filemime = mimeType.name();
if (!Core::IsMimeSticker(filemime)) { if (!Core::IsMimeSticker(filemime)
&& fullimageformat != u"jpeg"_q) {
fullimage = Images::Opaque(std::move(fullimage)); fullimage = Images::Opaque(std::move(fullimage));
fullimagebytes = fullimageformat = QByteArray();
} }
if (filemime == "image/jpeg") { if (filemime == "image/jpeg") {
filename = filedialogDefaultName(qsl("photo"), qsl(".jpg"), QString(), true); filename = filedialogDefaultName(qsl("photo"), qsl(".jpg"), QString(), true);
@ -764,6 +815,8 @@ void FileLoadTask::process(Args &&args) {
if (auto image = std::get_if<Ui::PreparedFileInformation::Image>( if (auto image = std::get_if<Ui::PreparedFileInformation::Image>(
&_information->media)) { &_information->media)) {
fullimage = base::take(image->data); fullimage = base::take(image->data);
fullimagebytes = base::take(image->bytes);
fullimageformat = base::take(image->format);
} }
} }
if (!fullimage.isNull() && fullimage.width() > 0) { if (!fullimage.isNull() && fullimage.width() > 0) {
@ -786,6 +839,7 @@ void FileLoadTask::process(Args &&args) {
filesize = _content.size(); filesize = _content.size();
} }
fullimage = Images::Opaque(std::move(fullimage)); fullimage = Images::Opaque(std::move(fullimage));
fullimagebytes = fullimageformat = QByteArray();
} }
} }
_result->filesize = (int32)qMin(filesize, qint64(INT_MAX)); _result->filesize = (int32)qMin(filesize, qint64(INT_MAX));
@ -878,18 +932,14 @@ void FileLoadTask::process(Args &&args) {
} else if (filemime.startsWith(u"image/"_q) } else if (filemime.startsWith(u"image/"_q)
&& _type != SendMediaType::File) { && _type != SendMediaType::File) {
auto medium = (w > 320 || h > 320) ? fullimage.scaled(320, 320, Qt::KeepAspectRatio, Qt::SmoothTransformation) : fullimage; auto medium = (w > 320 || h > 320) ? fullimage.scaled(320, 320, Qt::KeepAspectRatio, Qt::SmoothTransformation) : fullimage;
auto full = (w > 1280 || h > 1280) ? fullimage.scaled(1280, 1280, Qt::KeepAspectRatio, Qt::SmoothTransformation) : fullimage;
{ const auto downscaled = (w > 1280 || h > 1280);
// We have an example of dark .png image that when being sent without auto full = downscaled ? fullimage.scaled(1280, 1280, Qt::KeepAspectRatio, Qt::SmoothTransformation) : fullimage;
// removing its color space is displayed fine on tdesktop, but with if (downscaled) {
// a light gray background on mobile apps. fullimagebytes = fullimageformat = QByteArray();
full.setColorSpace(QColorSpace());
QBuffer buffer(&filedata);
QImageWriter writer(&buffer, "JPEG");
writer.setQuality(87);
writer.setProgressiveScanWrite(true);
writer.write(full);
} }
filedata = ComputePhotoJpegBytes(full, fullimagebytes, fullimageformat);
photoThumbs.emplace('m', PreparedPhotoThumb{ .image = medium }); photoThumbs.emplace('m', PreparedPhotoThumb{ .image = medium });
photoSizes.push_back(MTP_photoSize(MTP_string("m"), MTP_int(medium.width()), MTP_int(medium.height()), MTP_int(0))); photoSizes.push_back(MTP_photoSize(MTP_string("m"), MTP_int(medium.width()), MTP_int(medium.height()), MTP_int(0)));

View file

@ -263,7 +263,9 @@ public:
static bool FillImageInformation( static bool FillImageInformation(
QImage &&image, QImage &&image,
bool animated, bool animated,
std::unique_ptr<Ui::PreparedFileInformation> &result); std::unique_ptr<Ui::PreparedFileInformation> &result,
QByteArray content = {},
QByteArray format = {});
FileLoadTask( FileLoadTask(
not_null<Main::Session*> session, not_null<Main::Session*> session,

View file

@ -20,6 +20,8 @@ class SendFilesWay;
struct PreparedFileInformation { struct PreparedFileInformation {
struct Image { struct Image {
QImage data; QImage data;
QByteArray bytes;
QByteArray format;
bool animated = false; bool animated = false;
Editor::PhotoModifications modifications; Editor::PhotoModifications modifications;
}; };

@ -1 +1 @@
Subproject commit dd3cc5000e4f136ee198c5ace014640593e0aa2c Subproject commit 81e216f1cede22d30573233c36039716cc438b33