Compare commits

...

6 Commits

Author SHA1 Message Date
iequidoo
417349197c 123 2026-03-17 07:20:37 -03:00
72374
a25304604a feat: Do not scale images down by a small amount 2026-03-17 07:00:08 -03:00
72374
52e4e42373 feat: Change multiplier to 7/8 when scaling down avatars
The resolution-limits for avatar-images are currently 512x512 or 256x256.
Reducing the resolution further, to 2/3 each step, can reduce the quality
much more than is necessary to fit within the file-size-limits,
which are currently 60 kB or 20 kB.

An image made entirely of noise (which results in unusually
large file-sizes), encoded with jpeg-quality 75,
and 4:2:2-colour-subsampling (the format currently used
for encoding images), can be below 60 kB at 227x227.
For the lower file-size-limit of 20 kB,
such images can be too large at 170x170, but fit at 149x149.
More normal images will have a lower file-size at the same resolution.

Before this change, the target-resolutions for resampling were:
512x512 ->                       341x341 ->                       227x227.

And for the lower file-size-limit:
256x256 ->                       170x170 ->                       113x113.

After this change, the target-resolutions for resampling will be:
512x512 -> 448x448 -> 392x392 -> 343x343 -> 300x300 -> 262x262 -> 229x229.

And for the lower file-size-limit, those will be:
256x256 -> 224x224 -> 196x196 -> 171x171 -> 149x149 -> 130x130 -> 113x113.

This does add 2 steps between the previous target-resolutions,
while still reaching target-resolutions close to the previous ones,
to reduce situations in which the quality will be lower than before.
2026-03-17 07:00:08 -03:00
72374
18ed89abd5 feat: Increase the resolution-limit WORSE_AVATAR_SIZE from 128 to 256
The file-size of many images will already be smaller than 20 kB,
when encoded at 256x256, and it can be a large improvement in quality.
2026-03-17 07:00:08 -03:00
72374
89a281dd27 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.
2026-03-17 07:00:08 -03:00
72374
456891a0af refactor: Move the definition of the target_wh-variable
to the only part of the function where it is used.
2026-03-17 07:00:08 -03:00
5 changed files with 62 additions and 21 deletions

View File

@@ -389,14 +389,9 @@ impl<'a> BlobObject<'a> {
_ => img,
};
// max_wh is the maximum image width and height, i.e. the resolution-limit.
// target_wh target-resolution for resizing the image.
// max_wh is the maximum image width and height, i.e. the resolution-limit,
// as set by `Config::MediaQuality`.
let exceeds_wh = img.width() > max_wh || img.height() > max_wh;
let mut target_wh = if exceeds_wh {
max_wh
} else {
max(img.width(), img.height())
};
let exceeds_max_bytes = nr_bytes > max_bytes as u64;
let jpeg_quality = 75;
@@ -435,6 +430,32 @@ impl<'a> BlobObject<'a> {
});
if do_scale {
let n_px_longest_side = 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 !is_avatar {
let n_all_px_sqrt = f64::from(img.width() * 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 =
(f64::from(n_px_longest_side) * (f64::from(max_wh) / n_all_px_sqrt)) as u32;
// Align (at least) two sides of the resampled image to a multiple of 8 pixels,
// to have fewer partially used JPEG-blocks (which represent 8x8 pixels each).
while !resolution_limit.is_multiple_of(8) {
resolution_limit -= 1
}
resolution_limit
} else {
max_wh
};
if target_wh > n_px_longest_side
|| !is_avatar && target_wh > (f64::from(n_px_longest_side) * 0.98) as u32
{
target_wh = n_px_longest_side;
};
loop {
if mem::take(&mut add_white_bg) {
self::add_white_bg(&mut img);
@@ -468,7 +489,7 @@ impl<'a> BlobObject<'a> {
));
}
target_wh = target_wh * 2 / 3;
target_wh = target_wh * 7 / 8;
} else {
info!(
context,

View File

@@ -258,6 +258,26 @@ async fn test_selfavatar_copy_without_recode() {
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_recode_image_long_narrow() {
let bytes = include_bytes!("../../test-data/image/screenshot.png");
SendImageCheckMediaquality {
viewtype: Viewtype::Image,
media_quality_config: "0",
bytes,
extension: "jpg",
original_width: 1920,
original_height: 1080,
compressed_width: 1872,
compressed_height: 1053,
..Default::default()
}
.test()
.await
.unwrap();
assert!(false);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_recode_image_1() {
let bytes = include_bytes!("../../test-data/image/avatar1000x1000.jpg");
@@ -384,8 +404,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()
@@ -475,8 +495,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()
@@ -495,8 +515,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()

View File

@@ -193,17 +193,17 @@ pub const DC_LP_AUTH_NORMAL: i32 = 0x4;
pub const DC_LP_AUTH_FLAGS: i32 = DC_LP_AUTH_OAUTH2 | DC_LP_AUTH_NORMAL;
// max. weight of images to send w/o recoding
pub const BALANCED_IMAGE_BYTES: usize = 500_000;
pub const BALANCED_IMAGE_BYTES: usize = 1;
pub const WORSE_IMAGE_BYTES: usize = 130_000;
// max. width/height and bytes of an avatar
pub(crate) const BALANCED_AVATAR_SIZE: u32 = 512;
pub(crate) const BALANCED_AVATAR_BYTES: usize = 60_000;
pub(crate) const WORSE_AVATAR_SIZE: u32 = 128;
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.
// 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 = 1408;
pub const WORSE_IMAGE_SIZE: u32 = 640;
/// Limit for received images size. Bigger images become `Viewtype::File` to avoid excessive memory

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 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(())
}

View File

@@ -393,9 +393,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(())
}