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.
This commit is contained in:
72374
2026-06-13 10:07:56 +02:00
committed by Hocuri
parent b215bc6d16
commit 906e99a6f7
4 changed files with 36 additions and 15 deletions
+26 -5
View File
@@ -1,6 +1,6 @@
//! # Blob directory management. //! # Blob directory management.
use std::cmp::max; use std::cmp::{max, min};
use std::io::{Cursor, Seek}; use std::io::{Cursor, Seek};
use std::iter::FusedIterator; use std::iter::FusedIterator;
use std::mem; use std::mem;
@@ -12,7 +12,7 @@ use futures::StreamExt;
use image::ImageReader; use image::ImageReader;
use image::{DynamicImage, GenericImage, GenericImageView, ImageFormat, Pixel, Rgba}; use image::{DynamicImage, GenericImage, GenericImageView, ImageFormat, Pixel, Rgba};
use image::{codecs::jpeg::JpegEncoder, metadata::Orientation}; use image::{codecs::jpeg::JpegEncoder, metadata::Orientation};
use num_traits::FromPrimitive; use num_traits::{FromPrimitive, cast};
use tokio::{fs, task}; use tokio::{fs, task};
use tokio_stream::wrappers::ReadDirStream; use tokio_stream::wrappers::ReadDirStream;
@@ -423,14 +423,35 @@ impl<'a> BlobObject<'a> {
}); });
if do_scale { 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, // 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. // so that the longest sides of the image match the target-resolution.
let mut target_wh = if exceeds_wh { let mut target_wh = if !is_avatar {
max_wh 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 { } 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 { loop {
if mem::take(&mut add_white_bg) { if mem::take(&mut add_white_bg) {
self::add_white_bg(&mut img); self::add_white_bg(&mut img);
+6 -6
View File
@@ -406,8 +406,8 @@ async fn test_recode_image_balanced_png() {
extension: "png", extension: "png",
original_width: 1920, original_width: 1920,
original_height: 1080, original_height: 1080,
compressed_width: constants::WORSE_IMAGE_SIZE, compressed_width: 848,
compressed_height: constants::WORSE_IMAGE_SIZE * 1080 / 1920, compressed_height: 477,
..Default::default() ..Default::default()
} }
.test() .test()
@@ -497,8 +497,8 @@ async fn test_recode_image_rgba_png_to_jpeg() {
extension: "png", extension: "png",
original_width: 1920, original_width: 1920,
original_height: 1080, original_height: 1080,
compressed_width: constants::WORSE_IMAGE_SIZE, compressed_width: 848,
compressed_height: constants::WORSE_IMAGE_SIZE * 1080 / 1920, compressed_height: 477,
..Default::default() ..Default::default()
} }
.test() .test()
@@ -517,8 +517,8 @@ async fn test_recode_image_huge_jpg() {
has_exif: true, has_exif: true,
original_width: 1920, original_width: 1920,
original_height: 1080, original_height: 1080,
compressed_width: constants::BALANCED_IMAGE_SIZE, compressed_width: 1704,
compressed_height: constants::BALANCED_IMAGE_SIZE * 1080 / 1920, compressed_height: 959,
..Default::default() ..Default::default()
} }
.test() .test()
+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 – 146.12 KiB]".to_owned()); assert_eq!(msg.get_text(), "test [Image – 228.45 KiB]".to_owned());
Ok(()) Ok(())
} }
+3 -3
View File
@@ -402,9 +402,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(149632)); assert_eq!(msg.get_filebytes(bob).await?, Some(233935));
assert_eq!(msg.get_height(), 1280); assert_eq!(msg.get_height(), 1704);
assert_eq!(msg.get_width(), 720); assert_eq!(msg.get_width(), 959);
Ok(()) Ok(())
} }