Remove metadata from avatars and JPEG images before sending (#4027)

If there's an Exif, rewrite the file to remove it. This implies recoding now though.
This commit is contained in:
iequidoo
2023-02-14 13:02:57 -03:00
committed by iequidoo
parent 5403fd849c
commit 350509d5d1
3 changed files with 110 additions and 64 deletions

View File

@@ -113,6 +113,7 @@
- Run `cargo-deny` in CI. #4101 - Run `cargo-deny` in CI. #4101
- Check provider database with CI. #4099 - Check provider database with CI. #4099
- Switch to DEFERRED transactions #4100 - Switch to DEFERRED transactions #4100
- Remove metadata from avatars and JPEG images before sending #4037
### Fixes ### Fixes
- Do not block async task executor while decrypting the messages. #4079 - Do not block async task executor while decrypting the messages. #4079

View File

@@ -371,7 +371,8 @@ impl<'a> BlobObject<'a> {
) -> Result<Option<String>> { ) -> Result<Option<String>> {
tokio::task::block_in_place(move || { tokio::task::block_in_place(move || {
let mut img = image::open(&blob_abs).context("image recode failure")?; let mut img = image::open(&blob_abs).context("image recode failure")?;
let orientation = self.get_exif_orientation(context); let exif = self.get_exif().ok();
let orientation = exif.as_ref().map(|exif| exif_orientation(exif, context));
let mut encoded = Vec::new(); let mut encoded = Vec::new();
let mut changed_name = None; let mut changed_name = None;
@@ -379,53 +380,55 @@ impl<'a> BlobObject<'a> {
let do_scale = let do_scale =
exceeds_width || encoded_img_exceeds_bytes(context, &img, max_bytes, &mut encoded)?; exceeds_width || encoded_img_exceeds_bytes(context, &img, max_bytes, &mut encoded)?;
let do_rotate = matches!(orientation, Ok(90) | Ok(180) | Ok(270)); let do_rotate = matches!(orientation, Some(90) | Some(180) | Some(270));
if do_scale || do_rotate { if do_rotate {
if do_rotate { img = match orientation {
img = match orientation { Some(90) => img.rotate90(),
Ok(90) => img.rotate90(), Some(180) => img.rotate180(),
Ok(180) => img.rotate180(), Some(270) => img.rotate270(),
Ok(270) => img.rotate270(), _ => img,
_ => img, }
} }
if do_scale {
if !exceeds_width {
// The image is already smaller than img_wh, but exceeds max_bytes
// We can directly start with trying to scale down to 2/3 of its current width
img_wh = max(img.width(), img.height()) * 2 / 3
} }
if do_scale { loop {
if !exceeds_width { let new_img = img.thumbnail(img_wh, img_wh);
// The image is already smaller than img_wh, but exceeds max_bytes
// We can directly start with trying to scale down to 2/3 of its current width
img_wh = max(img.width(), img.height()) * 2 / 3
}
loop { if encoded_img_exceeds_bytes(context, &new_img, max_bytes, &mut encoded)? {
let new_img = img.thumbnail(img_wh, img_wh); if img_wh < 20 {
return Err(format_err!(
if encoded_img_exceeds_bytes(context, &new_img, max_bytes, &mut encoded)? { "Failed to scale image to below {}B.",
if img_wh < 20 { max_bytes.unwrap_or_default()
return Err(format_err!( ));
"Failed to scale image to below {}B",
max_bytes.unwrap_or_default()
));
}
img_wh = img_wh * 2 / 3;
} else {
if encoded.is_empty() {
encode_img(&new_img, &mut encoded)?;
}
info!(
context,
"Final scaled-down image size: {}B ({}px).",
encoded.len(),
img_wh
);
break;
} }
img_wh = img_wh * 2 / 3;
} else {
if encoded.is_empty() {
encode_img(&new_img, &mut encoded)?;
}
info!(
context,
"Final scaled-down image size: {}B ({}px).",
encoded.len(),
img_wh
);
break;
} }
} }
}
// We also need to rewrite the file to remove metadata such as location, camera model,
// etc. if any
if do_rotate || do_scale || exif.is_some() {
// The file format is JPEG now, we may have to change the file extension // The file format is JPEG now, we may have to change the file extension
if !matches!(ImageFormat::from_path(&blob_abs), Ok(ImageFormat::Jpeg)) { if !matches!(ImageFormat::from_path(&blob_abs), Ok(ImageFormat::Jpeg)) {
blob_abs = blob_abs.with_extension("jpg"); blob_abs = blob_abs.with_extension("jpg");
@@ -446,25 +449,28 @@ impl<'a> BlobObject<'a> {
}) })
} }
pub fn get_exif_orientation(&self, context: &Context) -> Result<i32> { pub fn get_exif(&self) -> Result<exif::Exif> {
let file = std::fs::File::open(self.to_abs_path())?; let file = std::fs::File::open(self.to_abs_path())?;
let mut bufreader = std::io::BufReader::new(&file); let mut bufreader = std::io::BufReader::new(&file);
let exifreader = exif::Reader::new(); let exifreader = exif::Reader::new();
let exif = exifreader.read_from_container(&mut bufreader)?; Ok(exifreader.read_from_container(&mut bufreader)?)
if let Some(orientation) = exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY) {
// possible orientation values are described at http://sylvana.net/jpegcrop/exif_orientation.html
// we only use rotation, in practise, flipping is not used.
match orientation.value.get_uint(0) {
Some(3) => return Ok(180),
Some(6) => return Ok(90),
Some(8) => return Ok(270),
other => warn!(context, "Exif orientation value ignored: {other:?}."),
}
}
Ok(0)
} }
} }
fn exif_orientation(exif: &exif::Exif, context: &Context) -> i32 {
if let Some(orientation) = exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY) {
// possible orientation values are described at http://sylvana.net/jpegcrop/exif_orientation.html
// we only use rotation, in practise, flipping is not used.
match orientation.value.get_uint(0) {
Some(3) => return 180,
Some(6) => return 90,
Some(8) => return 270,
other => warn!(context, "Exif orientation value ignored: {other:?}."),
}
}
0
}
impl<'a> fmt::Display for BlobObject<'a> { impl<'a> fmt::Display for BlobObject<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "$BLOBDIR/{}", self.name) write!(f, "$BLOBDIR/{}", self.name)
@@ -880,12 +886,22 @@ 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");
// BALANCED_IMAGE_SIZE > 1000, the original image size, so the image is not scaled down: // BALANCED_IMAGE_SIZE > 1000, the original image size, so the image is not scaled down:
send_image_check_mediaquality(Some("0"), bytes, 1000, 1000, 0, 1000, 1000) send_image_check_mediaquality(
.await Some("0"),
.unwrap(); bytes,
true, // has Exif
1000,
1000,
0,
1000,
1000,
)
.await
.unwrap();
send_image_check_mediaquality( send_image_check_mediaquality(
Some("1"), Some("1"),
bytes, bytes,
true, // has Exif
1000, 1000,
1000, 1000,
0, 0,
@@ -903,6 +919,7 @@ mod tests {
let img_rotated = send_image_check_mediaquality( let img_rotated = send_image_check_mediaquality(
Some("0"), Some("0"),
bytes, bytes,
true, // has Exif
2000, 2000,
1800, 1800,
270, 270,
@@ -926,6 +943,7 @@ mod tests {
let img_rotated = send_image_check_mediaquality( let img_rotated = send_image_check_mediaquality(
Some("0"), Some("0"),
&bytes2, &bytes2,
false, // no Exif
BALANCED_IMAGE_SIZE * 1800 / 2000, BALANCED_IMAGE_SIZE * 1800 / 2000,
BALANCED_IMAGE_SIZE, BALANCED_IMAGE_SIZE,
0, 0,
@@ -940,6 +958,7 @@ mod tests {
let img_rotated = send_image_check_mediaquality( let img_rotated = send_image_check_mediaquality(
Some("1"), Some("1"),
&bytes, &bytes,
false, // no Exif
BALANCED_IMAGE_SIZE * 1800 / 2000, BALANCED_IMAGE_SIZE * 1800 / 2000,
BALANCED_IMAGE_SIZE, BALANCED_IMAGE_SIZE,
0, 0,
@@ -956,15 +975,33 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_recode_image_3() { async fn test_recode_image_3() {
let bytes = include_bytes!("../test-data/image/rectangle200x180-rotated.jpg"); let bytes = include_bytes!("../test-data/image/rectangle200x180-rotated.jpg");
let img_rotated = send_image_check_mediaquality(Some("0"), bytes, 200, 180, 270, 180, 200) let img_rotated = send_image_check_mediaquality(
.await Some("0"),
.unwrap(); bytes,
true, // has Exif
200,
180,
270,
180,
200,
)
.await
.unwrap();
assert_correct_rotation(&img_rotated); assert_correct_rotation(&img_rotated);
let bytes = include_bytes!("../test-data/image/rectangle200x180-rotated.jpg"); let bytes = include_bytes!("../test-data/image/rectangle200x180-rotated.jpg");
let img_rotated = send_image_check_mediaquality(Some("1"), bytes, 200, 180, 270, 180, 200) let img_rotated = send_image_check_mediaquality(
.await Some("1"),
.unwrap(); bytes,
true, // has Exif
200,
180,
270,
180,
200,
)
.await
.unwrap();
assert_correct_rotation(&img_rotated); assert_correct_rotation(&img_rotated);
} }
@@ -985,9 +1022,11 @@ mod tests {
assert_eq!(luma, 0); assert_eq!(luma, 0);
} }
#[allow(clippy::too_many_arguments)]
async fn send_image_check_mediaquality( async fn send_image_check_mediaquality(
media_quality_config: Option<&str>, media_quality_config: Option<&str>,
bytes: &[u8], bytes: &[u8],
has_exif: bool,
original_width: u32, original_width: u32,
original_height: u32, original_height: u32,
orientation: i32, orientation: i32,
@@ -1007,7 +1046,13 @@ mod tests {
check_image_size(&file, original_width, original_height); check_image_size(&file, original_width, original_height);
let blob = BlobObject::new_from_path(&alice, &file).await?; let blob = BlobObject::new_from_path(&alice, &file).await?;
assert_eq!(blob.get_exif_orientation(&alice).unwrap_or(0), orientation); let exif = blob.get_exif();
if has_exif {
let exif = exif.unwrap();
assert_eq!(exif_orientation(&exif, &alice), orientation);
} else {
assert!(exif.is_err());
}
let mut msg = Message::new(Viewtype::Image); let mut msg = Message::new(Viewtype::Image);
msg.set_file(file.to_str().unwrap(), None); msg.set_file(file.to_str().unwrap(), None);
@@ -1028,7 +1073,7 @@ mod tests {
let file = bob_msg.get_file(&bob).unwrap(); let file = bob_msg.get_file(&bob).unwrap();
let blob = BlobObject::new_from_path(&bob, &file).await?; let blob = BlobObject::new_from_path(&bob, &file).await?;
assert_eq!(blob.get_exif_orientation(&bob).unwrap_or(0), 0); assert!(blob.get_exif().is_err());
let img = check_image_size(file, compressed_width, compressed_height); let img = check_image_size(file, compressed_width, compressed_height);
Ok(img) Ok(img)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB