Skip to content

Commit f79c652

Browse files
committed
Correctly compute size of avatars, add simple test
1 parent 4b862db commit f79c652

File tree

2 files changed

+99
-23
lines changed

2 files changed

+99
-23
lines changed

src/blob.rs

Lines changed: 71 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use async_std::{fs, io};
99

1010
use anyhow::format_err;
1111
use anyhow::Error;
12+
use image::DynamicImage;
1213
use image::GenericImageView;
1314
use num_traits::FromPrimitive;
1415
use thiserror::Error;
@@ -19,6 +20,7 @@ use crate::constants::{
1920
WORSE_IMAGE_SIZE,
2021
};
2122
use crate::context::Context;
23+
use crate::dc_tools::count_bytes;
2224
use crate::events::EventType;
2325
use crate::message;
2426

@@ -430,34 +432,64 @@ impl<'a> BlobObject<'a> {
430432
})?;
431433
let orientation = self.get_exif_orientation(context);
432434

435+
fn exceeds_bytes(
436+
context: &Context,
437+
img: &DynamicImage,
438+
max_bytes: Option<usize>,
439+
) -> anyhow::Result<bool> {
440+
if let Some(max_bytes) = max_bytes {
441+
let size = count_bytes(img)?;
442+
if size > max_bytes {
443+
info!(
444+
context,
445+
"image size {}B ({}x{}px) exceeds {}B, need to scale down",
446+
size,
447+
img.width(),
448+
img.height(),
449+
max_bytes,
450+
);
451+
return Ok(true);
452+
}
453+
}
454+
Ok(false)
455+
};
433456
let exceeds_width = img.width() > img_wh || img.height() > img_wh;
457+
458+
let do_scale = exceeds_width || exceeds_bytes(context, &img, max_bytes)?;
434459
let do_rotate = matches!(orientation, Ok(90) | Ok(180) | Ok(270));
435-
let exceeds_bytes = if let Some(max_bytes) = max_bytes {
436-
img.as_bytes().len() > max_bytes
437-
} else {
438-
false
439-
};
440460

441-
if exceeds_width || do_rotate || exceeds_bytes {
442-
let mut scaled_img = None;
443-
if exceeds_width {
444-
scaled_img = Some(img.thumbnail(img_wh, img_wh));
445-
}
446-
if let Some(max_bytes) = max_bytes {
447-
while scaled_img.as_ref().unwrap_or(&img).as_bytes().len() > max_bytes {
448-
img_wh = img_wh * 2 / 3;
449-
if img_wh < 20 {
450-
Err(format_err!(
451-
"Image width is <20, but size is still {}",
452-
img.as_bytes().len()
453-
))?
461+
if do_scale || do_rotate {
462+
if do_scale {
463+
if !exceeds_width {
464+
// The image is already smaller than img_wh, but exceeds max_bytes
465+
// We can directly start with trying to scale down to 2/3 of its current width
466+
img_wh = img.width() * 2 / 3 // TODO should be {max or min}(img.width(), img.height())
467+
}
468+
469+
img = loop {
470+
let new_img = img.thumbnail(img_wh, img_wh);
471+
472+
if exceeds_bytes(context, &new_img, max_bytes)? {
473+
if img_wh < 20 {
474+
return Err(format_err!(
475+
"Failed to scale image to below {}B",
476+
max_bytes.unwrap_or_default()
477+
)
478+
.into());
479+
}
480+
481+
img_wh = img_wh * 2 / 3;
482+
} else {
483+
info!(
484+
context,
485+
"Final scaled-down image size: {}B ({}px)",
486+
count_bytes(&new_img)?,
487+
img_wh
488+
); // TODO dbg (?)
489+
break new_img;
454490
}
455-
scaled_img = Some(img.thumbnail(img_wh, img_wh));
456491
}
457492
}
458-
if let Some(scaled) = scaled_img {
459-
img = scaled;
460-
}
461493

462494
if do_rotate {
463495
img = match orientation {
@@ -768,9 +800,25 @@ mod tests {
768800
assert_eq!(img.width(), 1000);
769801
assert_eq!(img.height(), 1000);
770802

771-
let img = image::open(avatar_blob).unwrap();
803+
let img = image::open(&avatar_blob).unwrap();
772804
assert_eq!(img.width(), BALANCED_AVATAR_SIZE);
773805
assert_eq!(img.height(), BALANCED_AVATAR_SIZE);
806+
807+
async fn file_size(path_buf: &PathBuf) -> u64 {
808+
let file = File::open(path_buf).await.unwrap();
809+
file.metadata().await.unwrap().len()
810+
}
811+
812+
let blob = BlobObject::new_from_path(&t, &avatar_blob).await.unwrap();
813+
814+
blob.recode_to_size(&t, blob.to_abs_path(), 1000, Some(3000))
815+
.await
816+
.unwrap();
817+
assert!(file_size(&avatar_blob).await <= 3000);
818+
assert!(file_size(&avatar_blob).await > 2000);
819+
let img = image::open(&avatar_blob).unwrap();
820+
assert!(img.width() > 130);
821+
assert_eq!(img.width(), img.height());
774822
}
775823

776824
#[async_std::test]

src/dc_tools.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use async_std::{fs, io};
1414

1515
use anyhow::{bail, Error};
1616
use chrono::{Local, TimeZone};
17+
use image::DynamicImage;
1718
use rand::{thread_rng, Rng};
1819

1920
use crate::chat::{add_device_msg, add_device_msg_with_importance};
@@ -682,6 +683,33 @@ pub fn remove_subject_prefix(last_subject: &str) -> String {
682683
.to_string()
683684
}
684685

686+
struct ByteCounter {
687+
count: usize,
688+
}
689+
690+
impl ByteCounter {
691+
fn new() -> Self {
692+
ByteCounter { count: 0 }
693+
}
694+
}
695+
696+
impl std::io::Write for ByteCounter {
697+
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
698+
self.count += buf.len();
699+
Ok(buf.len())
700+
}
701+
702+
fn flush(&mut self) -> io::Result<()> {
703+
Ok(())
704+
}
705+
}
706+
707+
pub(crate) fn count_bytes(img: &DynamicImage) -> anyhow::Result<usize> {
708+
let mut writer = ByteCounter::new();
709+
img.write_to(&mut writer, image::ImageFormat::Jpeg)?;
710+
Ok(writer.count)
711+
}
712+
685713
#[cfg(test)]
686714
mod tests {
687715
#![allow(clippy::indexing_slicing)]

0 commit comments

Comments
 (0)