Skip to content

Commit 350509d

Browse files
committed
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.
1 parent 5403fd8 commit 350509d

File tree

3 files changed

+110
-64
lines changed

3 files changed

+110
-64
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@
113113
- Run `cargo-deny` in CI. #4101
114114
- Check provider database with CI. #4099
115115
- Switch to DEFERRED transactions #4100
116+
- Remove metadata from avatars and JPEG images before sending #4037
116117

117118
### Fixes
118119
- Do not block async task executor while decrypting the messages. #4079

src/blob.rs

+109-64
Original file line numberDiff line numberDiff line change
@@ -371,61 +371,64 @@ impl<'a> BlobObject<'a> {
371371
) -> Result<Option<String>> {
372372
tokio::task::block_in_place(move || {
373373
let mut img = image::open(&blob_abs).context("image recode failure")?;
374-
let orientation = self.get_exif_orientation(context);
374+
let exif = self.get_exif().ok();
375+
let orientation = exif.as_ref().map(|exif| exif_orientation(exif, context));
375376
let mut encoded = Vec::new();
376377
let mut changed_name = None;
377378

378379
let exceeds_width = img.width() > img_wh || img.height() > img_wh;
379380

380381
let do_scale =
381382
exceeds_width || encoded_img_exceeds_bytes(context, &img, max_bytes, &mut encoded)?;
382-
let do_rotate = matches!(orientation, Ok(90) | Ok(180) | Ok(270));
383-
384-
if do_scale || do_rotate {
385-
if do_rotate {
386-
img = match orientation {
387-
Ok(90) => img.rotate90(),
388-
Ok(180) => img.rotate180(),
389-
Ok(270) => img.rotate270(),
390-
_ => img,
391-
}
383+
let do_rotate = matches!(orientation, Some(90) | Some(180) | Some(270));
384+
385+
if do_rotate {
386+
img = match orientation {
387+
Some(90) => img.rotate90(),
388+
Some(180) => img.rotate180(),
389+
Some(270) => img.rotate270(),
390+
_ => img,
392391
}
392+
}
393393

394-
if do_scale {
395-
if !exceeds_width {
396-
// The image is already smaller than img_wh, but exceeds max_bytes
397-
// We can directly start with trying to scale down to 2/3 of its current width
398-
img_wh = max(img.width(), img.height()) * 2 / 3
399-
}
394+
if do_scale {
395+
if !exceeds_width {
396+
// The image is already smaller than img_wh, but exceeds max_bytes
397+
// We can directly start with trying to scale down to 2/3 of its current width
398+
img_wh = max(img.width(), img.height()) * 2 / 3
399+
}
400400

401-
loop {
402-
let new_img = img.thumbnail(img_wh, img_wh);
403-
404-
if encoded_img_exceeds_bytes(context, &new_img, max_bytes, &mut encoded)? {
405-
if img_wh < 20 {
406-
return Err(format_err!(
407-
"Failed to scale image to below {}B",
408-
max_bytes.unwrap_or_default()
409-
));
410-
}
411-
412-
img_wh = img_wh * 2 / 3;
413-
} else {
414-
if encoded.is_empty() {
415-
encode_img(&new_img, &mut encoded)?;
416-
}
417-
418-
info!(
419-
context,
420-
"Final scaled-down image size: {}B ({}px).",
421-
encoded.len(),
422-
img_wh
423-
);
424-
break;
401+
loop {
402+
let new_img = img.thumbnail(img_wh, img_wh);
403+
404+
if encoded_img_exceeds_bytes(context, &new_img, max_bytes, &mut encoded)? {
405+
if img_wh < 20 {
406+
return Err(format_err!(
407+
"Failed to scale image to below {}B.",
408+
max_bytes.unwrap_or_default()
409+
));
425410
}
411+
412+
img_wh = img_wh * 2 / 3;
413+
} else {
414+
if encoded.is_empty() {
415+
encode_img(&new_img, &mut encoded)?;
416+
}
417+
418+
info!(
419+
context,
420+
"Final scaled-down image size: {}B ({}px).",
421+
encoded.len(),
422+
img_wh
423+
);
424+
break;
426425
}
427426
}
427+
}
428428

429+
// We also need to rewrite the file to remove metadata such as location, camera model,
430+
// etc. if any
431+
if do_rotate || do_scale || exif.is_some() {
429432
// The file format is JPEG now, we may have to change the file extension
430433
if !matches!(ImageFormat::from_path(&blob_abs), Ok(ImageFormat::Jpeg)) {
431434
blob_abs = blob_abs.with_extension("jpg");
@@ -446,23 +449,26 @@ impl<'a> BlobObject<'a> {
446449
})
447450
}
448451

449-
pub fn get_exif_orientation(&self, context: &Context) -> Result<i32> {
452+
pub fn get_exif(&self) -> Result<exif::Exif> {
450453
let file = std::fs::File::open(self.to_abs_path())?;
451454
let mut bufreader = std::io::BufReader::new(&file);
452455
let exifreader = exif::Reader::new();
453-
let exif = exifreader.read_from_container(&mut bufreader)?;
454-
if let Some(orientation) = exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY) {
455-
// possible orientation values are described at http://sylvana.net/jpegcrop/exif_orientation.html
456-
// we only use rotation, in practise, flipping is not used.
457-
match orientation.value.get_uint(0) {
458-
Some(3) => return Ok(180),
459-
Some(6) => return Ok(90),
460-
Some(8) => return Ok(270),
461-
other => warn!(context, "Exif orientation value ignored: {other:?}."),
462-
}
456+
Ok(exifreader.read_from_container(&mut bufreader)?)
457+
}
458+
}
459+
460+
fn exif_orientation(exif: &exif::Exif, context: &Context) -> i32 {
461+
if let Some(orientation) = exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY) {
462+
// possible orientation values are described at http://sylvana.net/jpegcrop/exif_orientation.html
463+
// we only use rotation, in practise, flipping is not used.
464+
match orientation.value.get_uint(0) {
465+
Some(3) => return 180,
466+
Some(6) => return 90,
467+
Some(8) => return 270,
468+
other => warn!(context, "Exif orientation value ignored: {other:?}."),
463469
}
464-
Ok(0)
465470
}
471+
0
466472
}
467473

468474
impl<'a> fmt::Display for BlobObject<'a> {
@@ -880,12 +886,22 @@ mod tests {
880886
async fn test_recode_image_1() {
881887
let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
882888
// BALANCED_IMAGE_SIZE > 1000, the original image size, so the image is not scaled down:
883-
send_image_check_mediaquality(Some("0"), bytes, 1000, 1000, 0, 1000, 1000)
884-
.await
885-
.unwrap();
889+
send_image_check_mediaquality(
890+
Some("0"),
891+
bytes,
892+
true, // has Exif
893+
1000,
894+
1000,
895+
0,
896+
1000,
897+
1000,
898+
)
899+
.await
900+
.unwrap();
886901
send_image_check_mediaquality(
887902
Some("1"),
888903
bytes,
904+
true, // has Exif
889905
1000,
890906
1000,
891907
0,
@@ -903,6 +919,7 @@ mod tests {
903919
let img_rotated = send_image_check_mediaquality(
904920
Some("0"),
905921
bytes,
922+
true, // has Exif
906923
2000,
907924
1800,
908925
270,
@@ -926,6 +943,7 @@ mod tests {
926943
let img_rotated = send_image_check_mediaquality(
927944
Some("0"),
928945
&bytes2,
946+
false, // no Exif
929947
BALANCED_IMAGE_SIZE * 1800 / 2000,
930948
BALANCED_IMAGE_SIZE,
931949
0,
@@ -940,6 +958,7 @@ mod tests {
940958
let img_rotated = send_image_check_mediaquality(
941959
Some("1"),
942960
&bytes,
961+
false, // no Exif
943962
BALANCED_IMAGE_SIZE * 1800 / 2000,
944963
BALANCED_IMAGE_SIZE,
945964
0,
@@ -956,15 +975,33 @@ mod tests {
956975
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
957976
async fn test_recode_image_3() {
958977
let bytes = include_bytes!("../test-data/image/rectangle200x180-rotated.jpg");
959-
let img_rotated = send_image_check_mediaquality(Some("0"), bytes, 200, 180, 270, 180, 200)
960-
.await
961-
.unwrap();
978+
let img_rotated = send_image_check_mediaquality(
979+
Some("0"),
980+
bytes,
981+
true, // has Exif
982+
200,
983+
180,
984+
270,
985+
180,
986+
200,
987+
)
988+
.await
989+
.unwrap();
962990
assert_correct_rotation(&img_rotated);
963991

964992
let bytes = include_bytes!("../test-data/image/rectangle200x180-rotated.jpg");
965-
let img_rotated = send_image_check_mediaquality(Some("1"), bytes, 200, 180, 270, 180, 200)
966-
.await
967-
.unwrap();
993+
let img_rotated = send_image_check_mediaquality(
994+
Some("1"),
995+
bytes,
996+
true, // has Exif
997+
200,
998+
180,
999+
270,
1000+
180,
1001+
200,
1002+
)
1003+
.await
1004+
.unwrap();
9681005
assert_correct_rotation(&img_rotated);
9691006
}
9701007

@@ -985,9 +1022,11 @@ mod tests {
9851022
assert_eq!(luma, 0);
9861023
}
9871024

1025+
#[allow(clippy::too_many_arguments)]
9881026
async fn send_image_check_mediaquality(
9891027
media_quality_config: Option<&str>,
9901028
bytes: &[u8],
1029+
has_exif: bool,
9911030
original_width: u32,
9921031
original_height: u32,
9931032
orientation: i32,
@@ -1007,7 +1046,13 @@ mod tests {
10071046
check_image_size(&file, original_width, original_height);
10081047

10091048
let blob = BlobObject::new_from_path(&alice, &file).await?;
1010-
assert_eq!(blob.get_exif_orientation(&alice).unwrap_or(0), orientation);
1049+
let exif = blob.get_exif();
1050+
if has_exif {
1051+
let exif = exif.unwrap();
1052+
assert_eq!(exif_orientation(&exif, &alice), orientation);
1053+
} else {
1054+
assert!(exif.is_err());
1055+
}
10111056

10121057
let mut msg = Message::new(Viewtype::Image);
10131058
msg.set_file(file.to_str().unwrap(), None);
@@ -1028,7 +1073,7 @@ mod tests {
10281073
let file = bob_msg.get_file(&bob).unwrap();
10291074

10301075
let blob = BlobObject::new_from_path(&bob, &file).await?;
1031-
assert_eq!(blob.get_exif_orientation(&bob).unwrap_or(0), 0);
1076+
assert!(blob.get_exif().is_err());
10321077

10331078
let img = check_image_size(file, compressed_width, compressed_height);
10341079
Ok(img)

test-data/image/avatar1000x1000.jpg

186 Bytes
Loading

0 commit comments

Comments
 (0)