feat: better quality of image recoding (#8682)

this PR introduces a better quality of image recoding, consuming max.
900k instead of 500k (average is much less). the PR roughly doubles the
number of pixels sent in an image.

the old 1280px were set 8 years ago,
data and storage has improved since then,
so it is reasonable to double the number if pixels used for sending an
image.

this will be a quality boost for many images,
while not resulting in a doubled size for all of them; many images will
only be a little larger in bytes, see test.

we could always go higher, of course, but it comes at costs of relay
storage and data, so we stay conservative, even in that increase.

the "worse quality" setting is not adapted on purpose, there we really
stay at the end of what is bearable :)

cc @adbenitez

---------

Co-authored-by: l <link2xt@testrun.org>
This commit is contained in:
biørn
2026-09-12 13:44:02 +00:00
committed by GitHub
co-authored by l
parent 9e8c3a63ad
commit 0b2051cda4
6 changed files with 14 additions and 13 deletions
+1 -1
View File
@@ -386,7 +386,7 @@ impl<'a> BlobObject<'a> {
let exceeds_wh = img.width() > max_wh || img.height() > max_wh; let exceeds_wh = img.width() > max_wh || img.height() > max_wh;
let exceeds_max_bytes = nr_bytes > max_bytes as u64; let exceeds_max_bytes = nr_bytes > max_bytes as u64;
let jpeg_quality = 75; let jpeg_quality = 75; // 70-80 is the sweet spot of quality vs. bytes/pixel. if one wants to spend more bytes in quality, better increase resolution
let ofmt = match fmt { let ofmt = match fmt {
ImageFormat::Png if !exceeds_max_bytes => ImageOutputFormat::Png, ImageFormat::Png if !exceeds_max_bytes => ImageOutputFormat::Png,
ImageFormat::Jpeg => { ImageFormat::Jpeg => {
+5 -5
View File
@@ -508,17 +508,17 @@ async fn test_recode_image_rgba_png_to_jpeg() {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_recode_image_huge_jpg() { async fn test_recode_image_huge_jpg() {
let bytes = include_bytes!("../../test-data/image/screenshot.jpg"); let bytes = include_bytes!("../../test-data/image/screenshot-2560x1440.jpg");
SendImageCheckMediaquality { SendImageCheckMediaquality {
viewtype: Viewtype::Image, viewtype: Viewtype::Image,
media_quality_config: "0", media_quality_config: "0",
bytes, bytes,
extension: "jpg", extension: "jpg",
has_exif: true, has_exif: true,
original_width: 1920, original_width: 2560,
original_height: 1080, original_height: 1440,
compressed_width: 1704, compressed_width: 2344,
compressed_height: 959, compressed_height: 1319,
..Default::default() ..Default::default()
} }
.test() .test()
+4 -3
View File
@@ -148,8 +148,9 @@ pub const DC_DESIRED_TEXT_LINE_LEN: usize = 100;
/// `char`s), not Unicode Grapheme Clusters. /// `char`s), not Unicode Grapheme Clusters.
pub const DC_DESIRED_TEXT_LEN: usize = DC_DESIRED_TEXT_LINE_LEN * DC_DESIRED_TEXT_LINES; pub const DC_DESIRED_TEXT_LEN: usize = DC_DESIRED_TEXT_LINE_LEN * DC_DESIRED_TEXT_LINES;
// max. weight of images to send w/o recoding /// max. weight of images to send w/o recoding.
pub const BALANCED_IMAGE_BYTES: usize = 500_000; // this is an estimation to the size we get when recoding high detail images.
pub const BALANCED_IMAGE_BYTES: usize = 940_000;
pub const WORSE_IMAGE_BYTES: usize = 130_000; pub const WORSE_IMAGE_BYTES: usize = 130_000;
// max. width/height and bytes of an avatar // max. width/height and bytes of an avatar
@@ -159,7 +160,7 @@ pub(crate) const WORSE_AVATAR_SIZE: u32 = 256;
pub(crate) const WORSE_AVATAR_BYTES: usize = 20_000; // this also fits to Outlook servers don't allowing headers larger than 32k. pub(crate) const WORSE_AVATAR_BYTES: usize = 20_000; // this also fits to Outlook servers don't allowing headers larger than 32k.
// max. width/height of images scaled down because of being too huge // max. width/height of images scaled down because of being too huge
pub const BALANCED_IMAGE_SIZE: u32 = 1280; pub const BALANCED_IMAGE_SIZE: u32 = 1760;
pub const WORSE_IMAGE_SIZE: u32 = 640; pub const WORSE_IMAGE_SIZE: u32 = 640;
/// Limit for received images size. Bigger images become `Viewtype::File` to avoid excessive memory /// Limit for received images size. Bigger images become `Viewtype::File` to avoid excessive memory
+1 -1
View File
@@ -34,7 +34,7 @@ async fn test_additional_text_on_different_viewtypes() -> Result<()> {
let (pre_message, _, _) = send_large_image_message(alice, a_group_id).await?; let (pre_message, _, _) = send_large_image_message(alice, a_group_id).await?;
let msg = bob.recv_msg(&pre_message).await; let msg = bob.recv_msg(&pre_message).await;
assert_eq!(msg.text, "test".to_owned()); assert_eq!(msg.text, "test".to_owned());
assert_eq!(msg.get_text(), "test [Image – 228.45 KiB]".to_owned()); assert_eq!(msg.get_text(), "test [Image – 275.46 KiB]".to_owned());
Ok(()) Ok(())
} }
+3 -3
View File
@@ -501,9 +501,9 @@ async fn test_receive_pre_message_image() -> Result<()> {
// test that metadata is correctly returned by methods // test that metadata is correctly returned by methods
assert_eq!(msg.get_post_message_viewtype(), Some(Viewtype::Image)); assert_eq!(msg.get_post_message_viewtype(), Some(Viewtype::Image));
// recoded image dimensions // recoded image dimensions
assert_eq!(msg.get_filebytes(bob).await?, Some(233935)); assert_eq!(msg.get_filebytes(bob).await?, Some(282068));
assert_eq!(msg.get_height(), 1704); assert_eq!(msg.get_height(), 1920);
assert_eq!(msg.get_width(), 959); assert_eq!(msg.get_width(), 1080);
Ok(()) Ok(())
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB