mirror of
https://github.com/chatmail/core.git
synced 2026-05-02 21:06:31 +03:00
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:
95
src/blob.rs
95
src/blob.rs
@@ -9,7 +9,7 @@ use std::path::{Path, PathBuf};
|
|||||||
|
|
||||||
use anyhow::{format_err, Context as _, Result};
|
use anyhow::{format_err, Context as _, Result};
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use image::{DynamicImage, ImageFormat, ImageOutputFormat};
|
use image::{DynamicImage, GenericImageView, ImageFormat, ImageOutputFormat};
|
||||||
use num_traits::FromPrimitive;
|
use num_traits::FromPrimitive;
|
||||||
use tokio::io::AsyncWriteExt;
|
use tokio::io::AsyncWriteExt;
|
||||||
use tokio::{fs, io};
|
use tokio::{fs, io};
|
||||||
@@ -323,18 +323,35 @@ impl<'a> BlobObject<'a> {
|
|||||||
MediaQuality::Worse => constants::WORSE_AVATAR_SIZE,
|
MediaQuality::Worse => constants::WORSE_AVATAR_SIZE,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let maybe_sticker = &mut false;
|
||||||
let strict_limits = true;
|
let strict_limits = true;
|
||||||
// max_bytes is 20_000 bytes: Outlook servers don't allow headers larger than 32k.
|
// 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.
|
// 32 / 4 * 3 = 24k if you account for base64 encoding. To be safe, we reduced this to 20k.
|
||||||
if let Some(new_name) =
|
if let Some(new_name) = self.recode_to_size(
|
||||||
self.recode_to_size(context, blob_abs, img_wh, 20_000, strict_limits)?
|
context,
|
||||||
{
|
blob_abs,
|
||||||
|
maybe_sticker,
|
||||||
|
img_wh,
|
||||||
|
20_000,
|
||||||
|
strict_limits,
|
||||||
|
)? {
|
||||||
self.name = new_name;
|
self.name = new_name;
|
||||||
}
|
}
|
||||||
Ok(())
|
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 blob_abs = self.to_abs_path();
|
||||||
let (img_wh, max_bytes) =
|
let (img_wh, max_bytes) =
|
||||||
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
|
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),
|
MediaQuality::Worse => (constants::WORSE_IMAGE_SIZE, constants::WORSE_IMAGE_BYTES),
|
||||||
};
|
};
|
||||||
let strict_limits = false;
|
let strict_limits = false;
|
||||||
if let Some(new_name) =
|
if let Some(new_name) = self.recode_to_size(
|
||||||
self.recode_to_size(context, blob_abs, img_wh, max_bytes, strict_limits)?
|
context,
|
||||||
{
|
blob_abs,
|
||||||
|
maybe_sticker,
|
||||||
|
img_wh,
|
||||||
|
max_bytes,
|
||||||
|
strict_limits,
|
||||||
|
)? {
|
||||||
self.name = new_name;
|
self.name = new_name;
|
||||||
}
|
}
|
||||||
Ok(())
|
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
|
/// If `!strict_limits`, then if `max_bytes` is exceeded, reduce the image to `img_wh` and just
|
||||||
/// proceed with the result.
|
/// proceed with the result.
|
||||||
fn recode_to_size(
|
fn recode_to_size(
|
||||||
&self,
|
&mut self,
|
||||||
context: &Context,
|
context: &Context,
|
||||||
mut blob_abs: PathBuf,
|
mut blob_abs: PathBuf,
|
||||||
|
maybe_sticker: &mut bool,
|
||||||
mut img_wh: u32,
|
mut img_wh: u32,
|
||||||
max_bytes: usize,
|
max_bytes: usize,
|
||||||
strict_limits: bool,
|
strict_limits: bool,
|
||||||
@@ -372,6 +395,19 @@ impl<'a> BlobObject<'a> {
|
|||||||
let mut encoded = Vec::new();
|
let mut encoded = Vec::new();
|
||||||
let mut changed_name = None;
|
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 {
|
img = match orientation {
|
||||||
Some(90) => img.rotate90(),
|
Some(90) => img.rotate90(),
|
||||||
Some(180) => img.rotate180(),
|
Some(180) => img.rotate180(),
|
||||||
@@ -860,10 +896,18 @@ mod tests {
|
|||||||
file.metadata().await.unwrap().len()
|
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;
|
let strict_limits = true;
|
||||||
blob.recode_to_size(&t, blob.to_abs_path(), 1000, 3000, strict_limits)
|
blob.recode_to_size(
|
||||||
.unwrap();
|
&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 <= 3000);
|
||||||
assert!(file_size(&avatar_blob).await > 2000);
|
assert!(file_size(&avatar_blob).await > 2000);
|
||||||
tokio::task::block_in_place(move || {
|
tokio::task::block_in_place(move || {
|
||||||
@@ -923,6 +967,7 @@ mod tests {
|
|||||||
async fn test_recode_image_1() {
|
async fn test_recode_image_1() {
|
||||||
let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
|
let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
|
||||||
send_image_check_mediaquality(
|
send_image_check_mediaquality(
|
||||||
|
Viewtype::Image,
|
||||||
Some("0"),
|
Some("0"),
|
||||||
bytes,
|
bytes,
|
||||||
"jpg",
|
"jpg",
|
||||||
@@ -936,6 +981,7 @@ mod tests {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
send_image_check_mediaquality(
|
send_image_check_mediaquality(
|
||||||
|
Viewtype::Image,
|
||||||
Some("1"),
|
Some("1"),
|
||||||
bytes,
|
bytes,
|
||||||
"jpg",
|
"jpg",
|
||||||
@@ -955,6 +1001,7 @@ mod tests {
|
|||||||
// The "-rotated" files are rotated by 270 degrees using the Exif metadata
|
// The "-rotated" files are rotated by 270 degrees using the Exif metadata
|
||||||
let bytes = include_bytes!("../test-data/image/rectangle2000x1800-rotated.jpg");
|
let bytes = include_bytes!("../test-data/image/rectangle2000x1800-rotated.jpg");
|
||||||
let img_rotated = send_image_check_mediaquality(
|
let img_rotated = send_image_check_mediaquality(
|
||||||
|
Viewtype::Image,
|
||||||
Some("0"),
|
Some("0"),
|
||||||
bytes,
|
bytes,
|
||||||
"jpg",
|
"jpg",
|
||||||
@@ -974,6 +1021,7 @@ mod tests {
|
|||||||
let bytes = buf.into_inner();
|
let bytes = buf.into_inner();
|
||||||
|
|
||||||
let img_rotated = send_image_check_mediaquality(
|
let img_rotated = send_image_check_mediaquality(
|
||||||
|
Viewtype::Image,
|
||||||
Some("1"),
|
Some("1"),
|
||||||
&bytes,
|
&bytes,
|
||||||
"jpg",
|
"jpg",
|
||||||
@@ -994,6 +1042,7 @@ mod tests {
|
|||||||
let bytes = include_bytes!("../test-data/image/screenshot.png");
|
let bytes = include_bytes!("../test-data/image/screenshot.png");
|
||||||
|
|
||||||
send_image_check_mediaquality(
|
send_image_check_mediaquality(
|
||||||
|
Viewtype::Image,
|
||||||
Some("0"),
|
Some("0"),
|
||||||
bytes,
|
bytes,
|
||||||
"png",
|
"png",
|
||||||
@@ -1008,6 +1057,7 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
send_image_check_mediaquality(
|
send_image_check_mediaquality(
|
||||||
|
Viewtype::Image,
|
||||||
Some("1"),
|
Some("1"),
|
||||||
bytes,
|
bytes,
|
||||||
"png",
|
"png",
|
||||||
@@ -1020,12 +1070,29 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.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)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn test_recode_image_huge_jpg() {
|
async fn test_recode_image_huge_jpg() {
|
||||||
let bytes = include_bytes!("../test-data/image/screenshot.jpg");
|
let bytes = include_bytes!("../test-data/image/screenshot.jpg");
|
||||||
send_image_check_mediaquality(
|
send_image_check_mediaquality(
|
||||||
|
Viewtype::Image,
|
||||||
Some("0"),
|
Some("0"),
|
||||||
bytes,
|
bytes,
|
||||||
"jpg",
|
"jpg",
|
||||||
@@ -1059,6 +1126,7 @@ mod tests {
|
|||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
async fn send_image_check_mediaquality(
|
async fn send_image_check_mediaquality(
|
||||||
|
viewtype: Viewtype,
|
||||||
media_quality_config: Option<&str>,
|
media_quality_config: Option<&str>,
|
||||||
bytes: &[u8],
|
bytes: &[u8],
|
||||||
extension: &str,
|
extension: &str,
|
||||||
@@ -1090,7 +1158,7 @@ mod tests {
|
|||||||
assert!(exif.is_none());
|
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);
|
msg.set_file(file.to_str().unwrap(), None);
|
||||||
let chat = alice.create_chat(&bob).await;
|
let chat = alice.create_chat(&bob).await;
|
||||||
let sent = alice.send_msg(chat.id, &mut msg).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;
|
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_width() as u32, compressed_width);
|
||||||
assert_eq!(bob_msg.get_height() as u32, compressed_height);
|
assert_eq!(bob_msg.get_height() as u32, compressed_height);
|
||||||
let file = bob_msg.get_file(&bob).unwrap();
|
let file = bob_msg.get_file(&bob).unwrap();
|
||||||
|
|||||||
51
src/chat.rs
51
src/chat.rs
@@ -2033,13 +2033,18 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|
|||||||
.await?
|
.await?
|
||||||
.with_context(|| format!("attachment missing for message of type #{}", msg.viewtype))?;
|
.with_context(|| format!("attachment missing for message of type #{}", msg.viewtype))?;
|
||||||
|
|
||||||
if msg.viewtype == Viewtype::Image {
|
let mut maybe_sticker = msg.viewtype == Viewtype::Sticker;
|
||||||
if let Err(err) = blob.recode_to_image_size(context).await {
|
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!(
|
warn!(
|
||||||
context,
|
context,
|
||||||
"Cannot recode image, using original data: {err:#}."
|
"Cannot recode image, using original data: {err:#}."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if !maybe_sticker {
|
||||||
|
msg.viewtype = Viewtype::Image;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
msg.param.set(Param::File, blob.as_name());
|
msg.param.set(Param::File, blob.as_name());
|
||||||
if let (Some(filename), Some(blob_ext)) = (msg.param.get(Param::Filename), blob.suffix()) {
|
if let (Some(filename), Some(blob_ext)) = (msg.param.get(Param::Filename), blob.suffix()) {
|
||||||
@@ -5510,7 +5515,13 @@ mod tests {
|
|||||||
Ok(())
|
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 alice = TestContext::new_alice().await;
|
||||||
let bob = TestContext::new_bob().await;
|
let bob = TestContext::new_bob().await;
|
||||||
let alice_chat = alice.create_chat(&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 sent_msg = alice.send_msg(alice_chat.id, &mut msg).await;
|
||||||
let mime = sent_msg.payload();
|
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;
|
let msg = bob.recv_msg(&sent_msg).await;
|
||||||
assert_eq!(msg.chat_id, bob_chat.id);
|
assert_eq!(msg.chat_id, bob_chat.id);
|
||||||
assert_eq!(msg.get_viewtype(), Viewtype::Sticker);
|
assert_eq!(msg.get_viewtype(), res_viewtype);
|
||||||
assert_eq!(msg.get_filename().unwrap(), filename);
|
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_width(), w);
|
||||||
assert_eq!(msg.get_height(), h);
|
assert_eq!(msg.get_height(), h);
|
||||||
assert!(msg.get_filebytes(&bob).await?.unwrap() > 250);
|
assert!(msg.get_filebytes(&bob).await?.unwrap() > 250);
|
||||||
@@ -5541,9 +5559,10 @@ mod tests {
|
|||||||
async fn test_sticker_png() -> Result<()> {
|
async fn test_sticker_png() -> Result<()> {
|
||||||
test_sticker(
|
test_sticker(
|
||||||
"sticker.png",
|
"sticker.png",
|
||||||
include_bytes!("../test-data/image/avatar64x64.png"),
|
include_bytes!("../test-data/image/logo.png"),
|
||||||
64,
|
Viewtype::Sticker,
|
||||||
64,
|
135,
|
||||||
|
135,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -5553,6 +5572,7 @@ mod tests {
|
|||||||
test_sticker(
|
test_sticker(
|
||||||
"sticker.jpg",
|
"sticker.jpg",
|
||||||
include_bytes!("../test-data/image/avatar1000x1000.jpg"),
|
include_bytes!("../test-data/image/avatar1000x1000.jpg"),
|
||||||
|
Viewtype::Image,
|
||||||
1000,
|
1000,
|
||||||
1000,
|
1000,
|
||||||
)
|
)
|
||||||
@@ -5563,9 +5583,10 @@ mod tests {
|
|||||||
async fn test_sticker_gif() -> Result<()> {
|
async fn test_sticker_gif() -> Result<()> {
|
||||||
test_sticker(
|
test_sticker(
|
||||||
"sticker.gif",
|
"sticker.gif",
|
||||||
include_bytes!("../test-data/image/image100x50.gif"),
|
include_bytes!("../test-data/image/logo.gif"),
|
||||||
100,
|
Viewtype::Sticker,
|
||||||
50,
|
135,
|
||||||
|
135,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -5579,8 +5600,8 @@ mod tests {
|
|||||||
let bob_chat = bob.create_chat(&alice).await;
|
let bob_chat = bob.create_chat(&alice).await;
|
||||||
|
|
||||||
// create sticker
|
// create sticker
|
||||||
let file_name = "sticker.jpg";
|
let file_name = "sticker.png";
|
||||||
let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
|
let bytes = include_bytes!("../test-data/image/logo.png");
|
||||||
let file = alice.get_blobdir().join(file_name);
|
let file = alice.get_blobdir().join(file_name);
|
||||||
tokio::fs::write(&file, bytes).await?;
|
tokio::fs::write(&file, bytes).await?;
|
||||||
let mut msg = Message::new(Viewtype::Sticker);
|
let mut msg = Message::new(Viewtype::Sticker);
|
||||||
@@ -6117,7 +6138,7 @@ mod tests {
|
|||||||
chat_id1,
|
chat_id1,
|
||||||
Viewtype::Sticker,
|
Viewtype::Sticker,
|
||||||
"b.png",
|
"b.png",
|
||||||
include_bytes!("../test-data/image/avatar64x64.png"),
|
include_bytes!("../test-data/image/logo.png"),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let second_image_msg_id = send_media(
|
let second_image_msg_id = send_media(
|
||||||
|
|||||||
BIN
test-data/image/logo.gif
Normal file
BIN
test-data/image/logo.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.8 KiB |
BIN
test-data/image/logo.png
Normal file
BIN
test-data/image/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
Reference in New Issue
Block a user