Skip to content

Commit 3c7a285

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 0ac6169 commit 3c7a285

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
@@ -14,6 +14,7 @@
1414
- Run `cargo-deny` in CI. #4101
1515
- Check provider database with CI. #4099
1616
- Switch to DEFERRED transactions #4100
17+
- Remove metadata from avatars and JPEG images before sending #4037
1718

1819
### Fixes
1920
- Do not block async task executor while decrypting the messages. #4079

src/blob.rs

+109-64
Original file line numberDiff line numberDiff line change
@@ -368,61 +368,64 @@ impl<'a> BlobObject<'a> {
368368
) -> Result<Option<String>> {
369369
tokio::task::block_in_place(move || {
370370
let mut img = image::open(&blob_abs).context("image recode failure")?;
371-
let orientation = self.get_exif_orientation(context);
371+
let exif = self.get_exif().ok();
372+
let orientation = exif.as_ref().map(|exif| exif_orientation(exif, context));
372373
let mut encoded = Vec::new();
373374
let mut changed_name = None;
374375

375376
let exceeds_width = img.width() > img_wh || img.height() > img_wh;
376377

377378
let do_scale =
378379
exceeds_width || encoded_img_exceeds_bytes(context, &img, max_bytes, &mut encoded)?;
379-
let do_rotate = matches!(orientation, Ok(90) | Ok(180) | Ok(270));
380-
381-
if do_scale || do_rotate {
382-
if do_rotate {
383-
img = match orientation {
384-
Ok(90) => img.rotate90(),
385-
Ok(180) => img.rotate180(),
386-
Ok(270) => img.rotate270(),
387-
_ => img,
388-
}
380+
let do_rotate = matches!(orientation, Some(90) | Some(180) | Some(270));
381+
382+
if do_rotate {
383+
img = match orientation {
384+
Some(90) => img.rotate90(),
385+
Some(180) => img.rotate180(),
386+
Some(270) => img.rotate270(),
387+
_ => img,
389388
}
389+
}
390390

391-
if do_scale {
392-
if !exceeds_width {
393-
// The image is already smaller than img_wh, but exceeds max_bytes
394-
// We can directly start with trying to scale down to 2/3 of its current width
395-
img_wh = max(img.width(), img.height()) * 2 / 3
396-
}
391+
if do_scale {
392+
if !exceeds_width {
393+
// The image is already smaller than img_wh, but exceeds max_bytes
394+
// We can directly start with trying to scale down to 2/3 of its current width
395+
img_wh = max(img.width(), img.height()) * 2 / 3
396+
}
397+
398+
loop {
399+
let new_img = img.thumbnail(img_wh, img_wh);
400+
401+
if encoded_img_exceeds_bytes(context, &new_img, max_bytes, &mut encoded)? {
402+
if img_wh < 20 {
403+
return Err(format_err!(
404+
"Failed to scale image to below {}B",
405+
max_bytes.unwrap_or_default()
406+
));
407+
}
397408

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

426+
// We also need to rewrite the file to remove metadata such as location, camera model,
427+
// etc. if any
428+
if do_rotate || do_scale || exif.is_some() {
426429
// The file format is JPEG now, we may have to change the file extension
427430
if !matches!(ImageFormat::from_path(&blob_abs), Ok(ImageFormat::Jpeg)) {
428431
blob_abs = blob_abs.with_extension("jpg");
@@ -443,23 +446,26 @@ impl<'a> BlobObject<'a> {
443446
})
444447
}
445448

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

465471
impl<'a> fmt::Display for BlobObject<'a> {
@@ -796,12 +802,22 @@ mod tests {
796802
async fn test_recode_image_1() {
797803
let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
798804
// BALANCED_IMAGE_SIZE > 1000, the original image size, so the image is not scaled down:
799-
send_image_check_mediaquality(Some("0"), bytes, 1000, 1000, 0, 1000, 1000)
800-
.await
801-
.unwrap();
805+
send_image_check_mediaquality(
806+
Some("0"),
807+
bytes,
808+
true, // has Exif
809+
1000,
810+
1000,
811+
0,
812+
1000,
813+
1000,
814+
)
815+
.await
816+
.unwrap();
802817
send_image_check_mediaquality(
803818
Some("1"),
804819
bytes,
820+
true, // has Exif
805821
1000,
806822
1000,
807823
0,
@@ -819,6 +835,7 @@ mod tests {
819835
let img_rotated = send_image_check_mediaquality(
820836
Some("0"),
821837
bytes,
838+
true, // has Exif
822839
2000,
823840
1800,
824841
270,
@@ -842,6 +859,7 @@ mod tests {
842859
let img_rotated = send_image_check_mediaquality(
843860
Some("0"),
844861
&bytes2,
862+
false, // no Exif
845863
BALANCED_IMAGE_SIZE * 1800 / 2000,
846864
BALANCED_IMAGE_SIZE,
847865
0,
@@ -856,6 +874,7 @@ mod tests {
856874
let img_rotated = send_image_check_mediaquality(
857875
Some("1"),
858876
&bytes,
877+
false, // no Exif
859878
BALANCED_IMAGE_SIZE * 1800 / 2000,
860879
BALANCED_IMAGE_SIZE,
861880
0,
@@ -872,15 +891,33 @@ mod tests {
872891
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
873892
async fn test_recode_image_3() {
874893
let bytes = include_bytes!("../test-data/image/rectangle200x180-rotated.jpg");
875-
let img_rotated = send_image_check_mediaquality(Some("0"), bytes, 200, 180, 270, 180, 200)
876-
.await
877-
.unwrap();
894+
let img_rotated = send_image_check_mediaquality(
895+
Some("0"),
896+
bytes,
897+
true, // has Exif
898+
200,
899+
180,
900+
270,
901+
180,
902+
200,
903+
)
904+
.await
905+
.unwrap();
878906
assert_correct_rotation(&img_rotated);
879907

880908
let bytes = include_bytes!("../test-data/image/rectangle200x180-rotated.jpg");
881-
let img_rotated = send_image_check_mediaquality(Some("1"), bytes, 200, 180, 270, 180, 200)
882-
.await
883-
.unwrap();
909+
let img_rotated = send_image_check_mediaquality(
910+
Some("1"),
911+
bytes,
912+
true, // has Exif
913+
200,
914+
180,
915+
270,
916+
180,
917+
200,
918+
)
919+
.await
920+
.unwrap();
884921
assert_correct_rotation(&img_rotated);
885922
}
886923

@@ -901,9 +938,11 @@ mod tests {
901938
assert_eq!(luma, 0);
902939
}
903940

941+
#[allow(clippy::too_many_arguments)]
904942
async fn send_image_check_mediaquality(
905943
media_quality_config: Option<&str>,
906944
bytes: &[u8],
945+
has_exif: bool,
907946
original_width: u32,
908947
original_height: u32,
909948
orientation: i32,
@@ -923,7 +962,13 @@ mod tests {
923962
check_image_size(&file, original_width, original_height);
924963

925964
let blob = BlobObject::new_from_path(&alice, &file).await?;
926-
assert_eq!(blob.get_exif_orientation(&alice).unwrap_or(0), orientation);
965+
let exif = blob.get_exif();
966+
if has_exif {
967+
let exif = exif.unwrap();
968+
assert_eq!(exif_orientation(&exif, &alice), orientation);
969+
} else {
970+
assert!(exif.is_err());
971+
}
927972

928973
let mut msg = Message::new(Viewtype::Image);
929974
msg.set_file(file.to_str().unwrap(), None);
@@ -944,7 +989,7 @@ mod tests {
944989
let file = bob_msg.get_file(&bob).unwrap();
945990

946991
let blob = BlobObject::new_from_path(&bob, &file).await?;
947-
assert_eq!(blob.get_exif_orientation(&bob).unwrap_or(0), 0);
992+
assert!(blob.get_exif().is_err());
948993

949994
let img = check_image_size(file, compressed_width, compressed_height);
950995
Ok(img)

test-data/image/avatar1000x1000.jpg

186 Bytes
Loading

0 commit comments

Comments
 (0)