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.
This commit is contained in:
iequidoo
2023-08-13 22:48:34 -03:00
committed by iequidoo
parent 62aed13880
commit ee28298d7f
4 changed files with 118 additions and 28 deletions

View File

@@ -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();

View File

@@ -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(