-
-
Notifications
You must be signed in to change notification settings - Fork 119
fix: Don't upscale images and test that image resolution isn't changed unnecessarily #7769
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
10e295c
e4e6d3c
b392227
f74cece
0bedc68
0731bd8
0bf3ec5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| //! # Blob directory management. | ||
|
|
||
| use std::cmp::max; | ||
| use std::io::{Cursor, Seek}; | ||
| use std::iter::FusedIterator; | ||
| use std::mem; | ||
|
|
@@ -255,7 +256,7 @@ impl<'a> BlobObject<'a> { | |
|
|
||
| /// Recode image to avatar size. | ||
| pub async fn recode_to_avatar_size(&mut self, context: &Context) -> Result<()> { | ||
| let (img_wh, max_bytes) = | ||
| let (max_wh, max_bytes) = | ||
| match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?) | ||
| .unwrap_or_default() | ||
| { | ||
|
|
@@ -272,7 +273,7 @@ impl<'a> BlobObject<'a> { | |
| let is_avatar = true; | ||
| self.check_or_recode_to_size( | ||
| context, None, // The name of an avatar doesn't matter | ||
| viewtype, img_wh, max_bytes, is_avatar, | ||
| viewtype, max_wh, max_bytes, is_avatar, | ||
| )?; | ||
|
|
||
| Ok(()) | ||
|
|
@@ -293,7 +294,7 @@ impl<'a> BlobObject<'a> { | |
| name: Option<String>, | ||
| viewtype: &mut Viewtype, | ||
| ) -> Result<String> { | ||
| let (img_wh, max_bytes) = | ||
| let (max_wh, max_bytes) = | ||
| match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?) | ||
| .unwrap_or_default() | ||
| { | ||
|
|
@@ -304,13 +305,15 @@ impl<'a> BlobObject<'a> { | |
| MediaQuality::Worse => (constants::WORSE_IMAGE_SIZE, constants::WORSE_IMAGE_BYTES), | ||
| }; | ||
| let is_avatar = false; | ||
| self.check_or_recode_to_size(context, name, viewtype, img_wh, max_bytes, is_avatar) | ||
| self.check_or_recode_to_size(context, name, viewtype, max_wh, max_bytes, is_avatar) | ||
| } | ||
|
|
||
| /// Checks or recodes the image so that it fits into limits on width/height and byte size. | ||
| /// Checks or recodes the image so that it fits into limits on width/height and/or byte size. | ||
| /// | ||
| /// If `!is_avatar`, then if `max_bytes` is exceeded, reduces the image to `img_wh` and proceeds | ||
| /// with the result without rechecking. | ||
| /// If `!is_avatar`, then if `max_bytes` is exceeded, reduces the image to `max_wh` and proceeds | ||
| /// with the result (even if `max_bytes` is still exceeded). | ||
| /// | ||
| /// If `is_avatar`, the resolution will be reduced in a loop until the image fits `max_bytes`. | ||
| /// | ||
| /// This modifies the blob object in-place. | ||
| /// | ||
|
|
@@ -323,7 +326,7 @@ impl<'a> BlobObject<'a> { | |
| context: &Context, | ||
| name: Option<String>, | ||
| viewtype: &mut Viewtype, | ||
| mut img_wh: u32, | ||
| max_wh: u32, | ||
| max_bytes: usize, | ||
| is_avatar: bool, | ||
| ) -> Result<String> { | ||
|
|
@@ -385,7 +388,14 @@ impl<'a> BlobObject<'a> { | |
| _ => img, | ||
| }; | ||
|
|
||
| let exceeds_wh = img.width() > img_wh || img.height() > img_wh; | ||
| // max_wh is the maximum image width and height, i.e. the resolution-limit. | ||
| // target_wh target-resolution for resizing the image. | ||
| 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; | ||
|
|
@@ -438,9 +448,9 @@ impl<'a> BlobObject<'a> { | |
| // usually has less pixels by cropping, UI that needs to wait anyways, | ||
| // and also benefits from slightly better (5%) encoding of Triangle-filtered images. | ||
| let new_img = if is_avatar { | ||
| img.resize(img_wh, img_wh, image::imageops::FilterType::Triangle) | ||
| img.resize(target_wh, target_wh, image::imageops::FilterType::Triangle) | ||
| } else { | ||
| img.thumbnail(img_wh, img_wh) | ||
| img.thumbnail(target_wh, target_wh) | ||
| }; | ||
|
|
||
| if encoded_img_exceeds_bytes( | ||
|
|
@@ -451,19 +461,19 @@ impl<'a> BlobObject<'a> { | |
| &mut encoded, | ||
| )? && is_avatar | ||
| { | ||
| if img_wh < 20 { | ||
| if target_wh < 20 { | ||
| return Err(format_err!( | ||
| "Failed to scale image to below {max_bytes}B.", | ||
| )); | ||
| } | ||
|
|
||
| img_wh = img_wh * 2 / 3; | ||
| target_wh = target_wh * 2 / 3; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd also remove this line from here and add if !encoded.is_empty() {
target_wh = target_wh * 2 / 3;
}to the beginning of the loop so that no extra iteration for avatars is done. It's a small optimization, but may make the code clearer, otherwise one may wonder why we recode avatars to the same size again.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure, this may easily lead to another issue where we unnecessarily reduce the target resolution. I anyways find the code block above hard to understand: let do_scale = exceeds_max_bytes
|| is_avatar
&& (exceeds_wh
|| exif.is_some() && {
if mem::take(&mut add_white_bg) {
self::add_white_bg(&mut img);
}
encoded_img_exceeds_bytes(
context,
&img,
ofmt.clone(),
max_bytes,
&mut encoded,
)?
});so... if the avatar does not exceed the max_bytes or width/height, but it has some exif, then we directly do an iteration of adding white background and recoding, and check the resulting bytes size. If the image is too big now (note that it wasn't too big before), then So, I guess that the code block is there to make really sure that the avatar file can't be too big in the end Which is good, of course, it's just hard to understand, and I am wary of adding code that depends on is behavior.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't the
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If it's an avatar w/o Exif, it should only be recoded if it exceeds any limits. White background is also not needed when not recoding an avatar, this is a w/a for transparent avatars recoded to JPEG, otherwise the
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That would still be the same after the change. I opened a PR with a short explanation for the change there: #7772
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Finally i think that it's fine to move the
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems correct. Something like this, before the beginning of the loop, as it only needs to be checked once: Which is similar to what you suggested in the previous PR, i think. Actually, not having this can result in quality-reduction, because, when the file-size-check happens, it does not only check the file-size, it also changes the image that will be used, which, depending on the encoder, can result in encoding the image again with jpeg-quality 75 (or not, if the encoder does detect that it is the same format and quality, and skips re-encoding). |
||
| } else { | ||
| info!( | ||
| context, | ||
| "Final scaled-down image size: {}B ({}px).", | ||
| encoded.len(), | ||
| img_wh | ||
| target_wh | ||
| ); | ||
| break; | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to be an outdated explanation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is only true for avatars, though. If not
is_avatar, then we'llbreakout of the loop after the first iteration.All images will be re-encoded if there is exif, not only avatars:
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed, i overlooked the
&& is avatarafterif encoded_img_exceeds_bytes.I guess i better wait with further suggestions until i can get Delta Chat to build with a local core (i tried that for multiple hours, without success) and verify the changes i want to suggest; my incorrect suggestions were probably rather distracting. ^^'
(I also forgot i could have tried a few things with the unmodified version.)
But anyway, thank you (you and the others working on Delta Chat and/or Chatmail) for making and improving Delta Chat and Chatmail.
It works well for me and my family-members so far (aside from my issues with the limited image-quality),
and the thing with multiple relays for a more peer-to-peer-like setup is finally something that is both independent and reliable enough, that i can comfortably recommend a messenger to friends and family.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the problem? And, are you trying to build DC Desktop, DC Android, or the REPL?
BTW, running the Rust tests locally should be just
cargo testcargo nextest run, and that way you can already somewhat verify changes by writing a Rust test, as I did in the PR here.It's a pleasure 😊
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is actually
cargo nextest run, withcargo testsome tests fail randomly because they are running in the same process.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DC Desktop.
After setting it up according to the documentation in README.md and UPDATE_CORE.md like this:
When trying to start it, the following error happens instead (paths shortened at
…) :Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/home/…/Delta_Chat/core/deltachat-rpc-server/npm-package/node_modules/@deltachat/jsonrpc-client/dist/deltachat.js' imported from /home/…/Delta_Chat/core/deltachat-rpc-server/npm-package/index.jsThe "dist"-folder does not exist.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you open an issue in https://github.com/deltachat/deltachat-desktop ?
It looks like documentation bug, there is some build step for the core missing.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was able to build and start Delta Chat Desktop, after doing
git checkout v1.58.2 && pnpm install && git checkout main && pnpm install. 1.58.2 is the last version before the upgrade to React 19.A dependency is unavailable since then. There is an issue-report about forking it: deltachat/deltachat-desktop#5935
I opened an issue-report there: deltachat/deltachat-desktop#5971