From 906e99a6f7c5bdb03f6f01f53b0256a3f0e9929c Mon Sep 17 00:00:00 2001 From: 72374 <250991390+72374@users.noreply.github.com> Date: Wed, 4 Feb 2026 03:07:28 +0100 Subject: [PATCH] feat: Make quality of images sent in chats more consistent Currently, the resolution of a resized image that was sent in a chat, depends on the aspect-ratio. Assuming the `balanced`-quality-setting is used, a square image, that is larger than the limits for resolution and file-size, will be resized to 1280x1280 (1,638,400 pixels), an image with an aspect-ratio of 16:9, will be resized to 1280x720 (921,600 pixels), and if the aspect-ratio is 32:9, to 1280x360 (460,800 pixels). This change makes it so, that the number of pixels, in images with different aspect-ratios, will be similar. --- src/blob.rs | 31 +++++++++++++++++++---- src/blob/blob_tests.rs | 12 ++++----- src/tests/pre_messages/additional_text.rs | 2 +- src/tests/pre_messages/receiving.rs | 6 ++--- 4 files changed, 36 insertions(+), 15 deletions(-) diff --git a/src/blob.rs b/src/blob.rs index 99e616dab..a60ebe65c 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -1,6 +1,6 @@ //! # Blob directory management. -use std::cmp::max; +use std::cmp::{max, min}; use std::io::{Cursor, Seek}; use std::iter::FusedIterator; use std::mem; @@ -12,7 +12,7 @@ use futures::StreamExt; use image::ImageReader; use image::{DynamicImage, GenericImage, GenericImageView, ImageFormat, Pixel, Rgba}; use image::{codecs::jpeg::JpegEncoder, metadata::Orientation}; -use num_traits::FromPrimitive; +use num_traits::{FromPrimitive, cast}; use tokio::{fs, task}; use tokio_stream::wrappers::ReadDirStream; @@ -423,14 +423,35 @@ impl<'a> BlobObject<'a> { }); if do_scale { + let longest_side_len = max(img.width(), img.height()); + // target_wh will be used as the target-resolution for resizing the image, // so that the longest sides of the image match the target-resolution. - let mut target_wh = if exceeds_wh { - max_wh + let mut target_wh = if !is_avatar { + let area_sqrt = (f64::from(img.width()) * f64::from(img.height())).sqrt(); + // Limit resolution to the number of pixels that fit within max_wh * max_wh, + // so that the image-quality does not depend on the aspect-ratio. + let mut resolution_limit: u32 = cast( + (f64::from(longest_side_len) * (f64::from(max_wh) / area_sqrt)).floor(), + ) + .unwrap_or(max_wh); + // Align at least one dimension of the resampled image to a multiple of 8 pixels, + // to have fewer partially used JPEG-blocks (which represent 8x8 pixels each). + if resolution_limit < longest_side_len && resolution_limit > 8 { + while !resolution_limit.is_multiple_of(8) { + resolution_limit -= 1 + } + } + resolution_limit } else { - max(img.width(), img.height()) + max_wh }; + target_wh = min(target_wh, longest_side_len); + + // For images in JPEG-format, 65535 pixels is the maximum resolution per dimension. + target_wh = min(target_wh, 65535); + loop { if mem::take(&mut add_white_bg) { self::add_white_bg(&mut img); diff --git a/src/blob/blob_tests.rs b/src/blob/blob_tests.rs index 014a2cce4..0b765d72a 100644 --- a/src/blob/blob_tests.rs +++ b/src/blob/blob_tests.rs @@ -406,8 +406,8 @@ async fn test_recode_image_balanced_png() { extension: "png", original_width: 1920, original_height: 1080, - compressed_width: constants::WORSE_IMAGE_SIZE, - compressed_height: constants::WORSE_IMAGE_SIZE * 1080 / 1920, + compressed_width: 848, + compressed_height: 477, ..Default::default() } .test() @@ -497,8 +497,8 @@ async fn test_recode_image_rgba_png_to_jpeg() { extension: "png", original_width: 1920, original_height: 1080, - compressed_width: constants::WORSE_IMAGE_SIZE, - compressed_height: constants::WORSE_IMAGE_SIZE * 1080 / 1920, + compressed_width: 848, + compressed_height: 477, ..Default::default() } .test() @@ -517,8 +517,8 @@ async fn test_recode_image_huge_jpg() { has_exif: true, original_width: 1920, original_height: 1080, - compressed_width: constants::BALANCED_IMAGE_SIZE, - compressed_height: constants::BALANCED_IMAGE_SIZE * 1080 / 1920, + compressed_width: 1704, + compressed_height: 959, ..Default::default() } .test() diff --git a/src/tests/pre_messages/additional_text.rs b/src/tests/pre_messages/additional_text.rs index 0af33ec8c..f37baa4e4 100644 --- a/src/tests/pre_messages/additional_text.rs +++ b/src/tests/pre_messages/additional_text.rs @@ -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 msg = bob.recv_msg(&pre_message).await; assert_eq!(msg.text, "test".to_owned()); - assert_eq!(msg.get_text(), "test [Image – 146.12 KiB]".to_owned()); + assert_eq!(msg.get_text(), "test [Image – 228.45 KiB]".to_owned()); Ok(()) } diff --git a/src/tests/pre_messages/receiving.rs b/src/tests/pre_messages/receiving.rs index 6bc1601bb..06f7abec5 100644 --- a/src/tests/pre_messages/receiving.rs +++ b/src/tests/pre_messages/receiving.rs @@ -402,9 +402,9 @@ async fn test_receive_pre_message_image() -> Result<()> { // test that metadata is correctly returned by methods assert_eq!(msg.get_post_message_viewtype(), Some(Viewtype::Image)); // recoded image dimensions - assert_eq!(msg.get_filebytes(bob).await?, Some(149632)); - assert_eq!(msg.get_height(), 1280); - assert_eq!(msg.get_width(), 720); + assert_eq!(msg.get_filebytes(bob).await?, Some(233935)); + assert_eq!(msg.get_height(), 1704); + assert_eq!(msg.get_width(), 959); Ok(()) }