From ee28298d7fd4576441cd5f0b4fa8e8d4bd3901bb Mon Sep 17 00:00:00 2001 From: iequidoo Date: Sun, 13 Aug 2023 22:48:34 -0300 Subject: [PATCH] fix: W/a sending images sent as stickers on some platforms (#4611) Check if a sticker has at least one fully transparent corner and otherwise change the Sticker type to Image. This would fix both Android and iOS at the same time and prevent similar bug on future platforms that may get this bug like Ubuntu Touch. --- src/blob.rs | 95 +++++++++++++++++++++++++++++++++------ src/chat.rs | 51 ++++++++++++++------- test-data/image/logo.gif | Bin 0 -> 5894 bytes test-data/image/logo.png | Bin 0 -> 13377 bytes 4 files changed, 118 insertions(+), 28 deletions(-) create mode 100644 test-data/image/logo.gif create mode 100644 test-data/image/logo.png diff --git a/src/blob.rs b/src/blob.rs index 579a2ad4e..a75021747 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -9,7 +9,7 @@ use std::path::{Path, PathBuf}; use anyhow::{format_err, Context as _, Result}; use futures::StreamExt; -use image::{DynamicImage, ImageFormat, ImageOutputFormat}; +use image::{DynamicImage, GenericImageView, ImageFormat, ImageOutputFormat}; use num_traits::FromPrimitive; use tokio::io::AsyncWriteExt; use tokio::{fs, io}; @@ -323,18 +323,35 @@ impl<'a> BlobObject<'a> { MediaQuality::Worse => constants::WORSE_AVATAR_SIZE, }; + let maybe_sticker = &mut false; let strict_limits = true; // max_bytes is 20_000 bytes: Outlook servers don't allow headers larger than 32k. // 32 / 4 * 3 = 24k if you account for base64 encoding. To be safe, we reduced this to 20k. - if let Some(new_name) = - self.recode_to_size(context, blob_abs, img_wh, 20_000, strict_limits)? - { + if let Some(new_name) = self.recode_to_size( + context, + blob_abs, + maybe_sticker, + img_wh, + 20_000, + strict_limits, + )? { self.name = new_name; } Ok(()) } - pub async fn recode_to_image_size(&mut self, context: &Context) -> Result<()> { + /// Recodes an image pointed by a [BlobObject] so that it fits into limits on the image width, + /// height and file size specified by the config. + /// + /// On some platforms images are passed to the core as [`crate::message::Viewtype::Sticker`] in + /// which case `maybe_sticker` flag should be set. We recheck if an image is a true sticker + /// assuming that it must have at least one fully transparent corner, otherwise this flag is + /// reset. + pub async fn recode_to_image_size( + &mut self, + context: &Context, + maybe_sticker: &mut bool, + ) -> Result<()> { let blob_abs = self.to_abs_path(); let (img_wh, max_bytes) = match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?) @@ -347,9 +364,14 @@ impl<'a> BlobObject<'a> { MediaQuality::Worse => (constants::WORSE_IMAGE_SIZE, constants::WORSE_IMAGE_BYTES), }; let strict_limits = false; - if let Some(new_name) = - self.recode_to_size(context, blob_abs, img_wh, max_bytes, strict_limits)? - { + if let Some(new_name) = self.recode_to_size( + context, + blob_abs, + maybe_sticker, + img_wh, + max_bytes, + strict_limits, + )? { self.name = new_name; } Ok(()) @@ -358,9 +380,10 @@ impl<'a> BlobObject<'a> { /// If `!strict_limits`, then if `max_bytes` is exceeded, reduce the image to `img_wh` and just /// proceed with the result. fn recode_to_size( - &self, + &mut self, context: &Context, mut blob_abs: PathBuf, + maybe_sticker: &mut bool, mut img_wh: u32, max_bytes: usize, strict_limits: bool, @@ -372,6 +395,19 @@ impl<'a> BlobObject<'a> { let mut encoded = Vec::new(); let mut changed_name = None; + if *maybe_sticker { + let x_max = img.width().saturating_sub(1); + let y_max = img.height().saturating_sub(1); + *maybe_sticker = img.in_bounds(x_max, y_max) + && (img.get_pixel(0, 0).0[3] == 0 + || img.get_pixel(x_max, 0).0[3] == 0 + || img.get_pixel(0, y_max).0[3] == 0 + || img.get_pixel(x_max, y_max).0[3] == 0); + } + if *maybe_sticker && exif.is_none() { + return Ok(None); + } + img = match orientation { Some(90) => img.rotate90(), Some(180) => img.rotate180(), @@ -860,10 +896,18 @@ mod tests { file.metadata().await.unwrap().len() } - let blob = BlobObject::new_from_path(&t, &avatar_blob).await.unwrap(); + let mut blob = BlobObject::new_from_path(&t, &avatar_blob).await.unwrap(); + let maybe_sticker = &mut false; let strict_limits = true; - blob.recode_to_size(&t, blob.to_abs_path(), 1000, 3000, strict_limits) - .unwrap(); + blob.recode_to_size( + &t, + blob.to_abs_path(), + maybe_sticker, + 1000, + 3000, + strict_limits, + ) + .unwrap(); assert!(file_size(&avatar_blob).await <= 3000); assert!(file_size(&avatar_blob).await > 2000); tokio::task::block_in_place(move || { @@ -923,6 +967,7 @@ mod tests { async fn test_recode_image_1() { let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg"); send_image_check_mediaquality( + Viewtype::Image, Some("0"), bytes, "jpg", @@ -936,6 +981,7 @@ mod tests { .await .unwrap(); send_image_check_mediaquality( + Viewtype::Image, Some("1"), bytes, "jpg", @@ -955,6 +1001,7 @@ mod tests { // The "-rotated" files are rotated by 270 degrees using the Exif metadata let bytes = include_bytes!("../test-data/image/rectangle2000x1800-rotated.jpg"); let img_rotated = send_image_check_mediaquality( + Viewtype::Image, Some("0"), bytes, "jpg", @@ -974,6 +1021,7 @@ mod tests { let bytes = buf.into_inner(); let img_rotated = send_image_check_mediaquality( + Viewtype::Image, Some("1"), &bytes, "jpg", @@ -994,6 +1042,7 @@ mod tests { let bytes = include_bytes!("../test-data/image/screenshot.png"); send_image_check_mediaquality( + Viewtype::Image, Some("0"), bytes, "png", @@ -1008,6 +1057,7 @@ mod tests { .unwrap(); send_image_check_mediaquality( + Viewtype::Image, Some("1"), bytes, "png", @@ -1020,12 +1070,29 @@ mod tests { ) .await .unwrap(); + + // This will be sent as Image, see [`BlobObject::maybe_sticker`] for explanation. + send_image_check_mediaquality( + Viewtype::Sticker, + Some("0"), + bytes, + "png", + false, // no Exif + 1920, + 1080, + 0, + 1920, + 1080, + ) + .await + .unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_recode_image_huge_jpg() { let bytes = include_bytes!("../test-data/image/screenshot.jpg"); send_image_check_mediaquality( + Viewtype::Image, Some("0"), bytes, "jpg", @@ -1059,6 +1126,7 @@ mod tests { #[allow(clippy::too_many_arguments)] async fn send_image_check_mediaquality( + viewtype: Viewtype, media_quality_config: Option<&str>, bytes: &[u8], extension: &str, @@ -1090,7 +1158,7 @@ mod tests { assert!(exif.is_none()); } - let mut msg = Message::new(Viewtype::Image); + let mut msg = Message::new(viewtype); msg.set_file(file.to_str().unwrap(), None); let chat = alice.create_chat(&bob).await; let sent = alice.send_msg(chat.id, &mut msg).await; @@ -1104,6 +1172,7 @@ mod tests { ); let bob_msg = bob.recv_msg(&sent).await; + assert_eq!(bob_msg.get_viewtype(), Viewtype::Image); assert_eq!(bob_msg.get_width() as u32, compressed_width); assert_eq!(bob_msg.get_height() as u32, compressed_height); let file = bob_msg.get_file(&bob).unwrap(); diff --git a/src/chat.rs b/src/chat.rs index ec5dac52b..27fa71c73 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -2033,13 +2033,18 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> { .await? .with_context(|| format!("attachment missing for message of type #{}", msg.viewtype))?; - if msg.viewtype == Viewtype::Image { - if let Err(err) = blob.recode_to_image_size(context).await { + let mut maybe_sticker = msg.viewtype == Viewtype::Sticker; + if msg.viewtype == Viewtype::Image || maybe_sticker { + // TODO: Ignore errors only if the image has no Exif. + if let Err(err) = blob.recode_to_image_size(context, &mut maybe_sticker).await { warn!( context, "Cannot recode image, using original data: {err:#}." ); } + if !maybe_sticker { + msg.viewtype = Viewtype::Image; + } } msg.param.set(Param::File, blob.as_name()); if let (Some(filename), Some(blob_ext)) = (msg.param.get(Param::Filename), blob.suffix()) { @@ -5510,7 +5515,13 @@ mod tests { Ok(()) } - async fn test_sticker(filename: &str, bytes: &[u8], w: i32, h: i32) -> Result<()> { + async fn test_sticker( + filename: &str, + bytes: &[u8], + res_viewtype: Viewtype, + w: i32, + h: i32, + ) -> Result<()> { let alice = TestContext::new_alice().await; let bob = TestContext::new_bob().await; let alice_chat = alice.create_chat(&bob).await; @@ -5524,12 +5535,19 @@ mod tests { let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await; let mime = sent_msg.payload(); - assert_eq!(mime.match_indices("Chat-Content: sticker").count(), 1); + if res_viewtype == Viewtype::Sticker { + assert_eq!(mime.match_indices("Chat-Content: sticker").count(), 1); + } let msg = bob.recv_msg(&sent_msg).await; assert_eq!(msg.chat_id, bob_chat.id); - assert_eq!(msg.get_viewtype(), Viewtype::Sticker); - assert_eq!(msg.get_filename().unwrap(), filename); + assert_eq!(msg.get_viewtype(), res_viewtype); + let msg_filename = msg.get_filename().unwrap(); + match res_viewtype { + Viewtype::Sticker => assert_eq!(msg_filename, filename), + Viewtype::Image => assert!(msg_filename.starts_with("image_")), + _ => panic!("Not implemented"), + } assert_eq!(msg.get_width(), w); assert_eq!(msg.get_height(), h); assert!(msg.get_filebytes(&bob).await?.unwrap() > 250); @@ -5541,9 +5559,10 @@ mod tests { async fn test_sticker_png() -> Result<()> { test_sticker( "sticker.png", - include_bytes!("../test-data/image/avatar64x64.png"), - 64, - 64, + include_bytes!("../test-data/image/logo.png"), + Viewtype::Sticker, + 135, + 135, ) .await } @@ -5553,6 +5572,7 @@ mod tests { test_sticker( "sticker.jpg", include_bytes!("../test-data/image/avatar1000x1000.jpg"), + Viewtype::Image, 1000, 1000, ) @@ -5563,9 +5583,10 @@ mod tests { async fn test_sticker_gif() -> Result<()> { test_sticker( "sticker.gif", - include_bytes!("../test-data/image/image100x50.gif"), - 100, - 50, + include_bytes!("../test-data/image/logo.gif"), + Viewtype::Sticker, + 135, + 135, ) .await } @@ -5579,8 +5600,8 @@ mod tests { let bob_chat = bob.create_chat(&alice).await; // create sticker - let file_name = "sticker.jpg"; - let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg"); + let file_name = "sticker.png"; + let bytes = include_bytes!("../test-data/image/logo.png"); let file = alice.get_blobdir().join(file_name); tokio::fs::write(&file, bytes).await?; let mut msg = Message::new(Viewtype::Sticker); @@ -6117,7 +6138,7 @@ mod tests { chat_id1, Viewtype::Sticker, "b.png", - include_bytes!("../test-data/image/avatar64x64.png"), + include_bytes!("../test-data/image/logo.png"), ) .await?; let second_image_msg_id = send_media( diff --git a/test-data/image/logo.gif b/test-data/image/logo.gif new file mode 100644 index 0000000000000000000000000000000000000000..f25b2d0ed74e393d7d46c8f9fff52c25238fdc52 GIT binary patch literal 5894 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 Mst8x8lrs