Skip to content

Conversation

@iequidoo
Copy link
Collaborator

@iequidoo iequidoo commented Feb 5, 2026

Instead of 2/3 which is not optimal for 512 px avatars usually passed to Core, use the sequence 3/4,
5/8, 4/8, 3/8... to reduce aliasing effects. Also divide the original avatar size, not the Core
constant (which is 512 px currently), this makes sense if e.g. 640 px avatars are passed.

Before, it was discussed that just 3/4 can be used. However:

  • If we repeat the reduction step, we get 3 << n as a numerator and this way accumulate aliasing
    effects. Better limit it to 5.
  • If a 640 px avatar is passed, giving that BALANCED_AVATAR_SIZE is 512, 3/4 gives 384 i.e. 3/5 of
    640 which is good but still worse than 3/4.

No test yet, but can add if the approach is agreed.

Instead of 2/3 which is not optimal for 512 px avatars usually passed to Core, use the sequence 3/4,
5/8, 4/8, 3/8... to reduce aliasing effects. Also divide the original avatar size, not the Core
constant (which is 512 px currently), this makes sense if e.g. 640 px avatars are passed.

Before, it was discussed that just 3/4 can be used. However:
- If we repeat the reduction step, we get `3 << n` as a numerator and this way accumulate aliasing
  effects. Better limit it to 5.
- If a 640 px avatar is passed, giving that `BALANCED_AVATAR_SIZE` is 512, 3/4 gives 384 i.e. 3/5 of
  640 which is good but still worse than 3/4.
@72374
Copy link
Contributor

72374 commented Feb 9, 2026

Note: The Android-app of Delta Chat does now use 512 * 512 for avatar-images.

Also, see: a32210d in #7822 .


The resampling ("resizing") is done from the original image, on each iteration, so aliasing-effects do not accumulate (here, img is the original, and resize returns a new image):

core/src/blob.rs

Lines 450 to 454 in 5028842

let new_img = if is_avatar {
img.resize(target_wh, target_wh, image::imageops::FilterType::Triangle)
} else {
img.thumbnail(target_wh, target_wh)
};

The encoded images are then made from the image resized from the original (new_img), on each iteration of the loop:

core/src/blob.rs

Lines 456 to 462 in 5028842

if encoded_img_exceeds_bytes(
context,
&new_img,
ofmt.clone(),
max_bytes,
&mut encoded,
)? && is_avatar

encoded_img_exceeds_bytes uses encode_img on line 671 (img here, is the image given to the function; in this situation new_img):

core/src/blob.rs

Lines 664 to 684 in 5028842

fn encoded_img_exceeds_bytes(
context: &Context,
img: &DynamicImage,
fmt: ImageOutputFormat,
max_bytes: usize,
encoded: &mut Vec<u8>,
) -> anyhow::Result<bool> {
encode_img(img, fmt, encoded)?;
if encoded.len() > max_bytes {
info!(
context,
"Image size {}B ({}x{}px) exceeds {}B, need to scale down.",
encoded.len(),
img.width(),
img.height(),
max_bytes,
);
return Ok(true);
}
Ok(false)
}

A previously encoded image (encoded) will be cleared on line 649 in encode_img, and the image, that will be encoded, will be made from the given image (which is new_img, resampled from the original on line 450, in this situation):

core/src/blob.rs

Lines 644 to 662 in 5028842

fn encode_img(
img: &DynamicImage,
fmt: ImageOutputFormat,
encoded: &mut Vec<u8>,
) -> anyhow::Result<()> {
encoded.clear();
let mut buf = Cursor::new(encoded);
match fmt {
ImageOutputFormat::Png => img.write_to(&mut buf, ImageFormat::Png)?,
ImageOutputFormat::Jpeg { quality } => {
let encoder = JpegEncoder::new_with_quality(&mut buf, quality);
// Convert image into RGB8 to avoid the error
// "The encoder or decoder for Jpeg does not support the color type Rgba8"
// (<https://github.com/image-rs/image/issues/2211>).
img.clone().into_rgb8().write_with_encoder(encoder)?;
}
}
Ok(())
}

@iequidoo
Copy link
Collaborator Author

iequidoo commented Feb 9, 2026

The resampling ("resizing") is done from the original image, on each iteration, so aliasing-effects do not accumulate

Ok, "accumulate" is probably a bad wording. I meant that if we need to resize a 512 px avatar, using e.g. 27/64 as a multiplier is worse than 1/2 or 3/8 in terms of aliasing effects. So the PR limits the numerator to 5.

@72374
Copy link
Contributor

72374 commented Feb 10, 2026

If i understand correctly, these would be the resolution-values used for resampling, if the file-size is too large at 512x512 (until the target-resolution is below 200 pixels in the first table, and 128 pixels in the second table; rounded down after every step; only resolutions below 512x512):

Resolution-change Original resolution 1 2 3 4 5 6 7
* 2 / 3 >= 512 341 227 151
This PR 512 384 320 256 192
This PR 1024 384 320 256 192
This PR 1080 405 337 270 202 168
This PR 1637 511 409 306 255 204 153
This PR 1920 480 360 300 240 180
This PR 2000 500 375 312 250 187
This PR 3584 448 336 280 224 168
* 7000 / 8005 >= 512 447 390 341 298 260 227 198
Resolution-change Original resolution 1 2 3 4 5 6
* 2 / 3 256 170 113
This PR 256 192 160 128 96
* 7000 / 8005 256 223 195 170 148 129 112

I see two issues with this change:

  1. If an avatar-image with a resolution of 512x512 (all avatar-images that will be set with the next version of the Android-app of Delta Chat) does not fit within the file-size-limit at 384x384, but would fit at 341x341, it will instead be resampled to 320x320.
    With the linear resampling that is used for avatar-images, aliasing is generally not very noticeable; at least less than a resolution-difference of ~20x20 pixels, with resolutions this low.
  2. The first target-resolutions below 512, can vary from 384 to 511, depending on the original resolution of the image, but not in a way that reliably results in higher resolutions for original images with a higher resolution.

Other than that, it would be a nice improvement in quality.

A simpler change, that would almost always improve quality, compared to using * 2 / 3, would be to change the resolution by * 7000 / 8005, per iteration. That would reach the previous integer-values almost exactly, so the resulting images would look at least almost as good as before, and often better.

I hope this is useful. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants