照片图片 Exif
通过手机相机或者数码相机拍摄的照片都带有 Exif 元数据信息,比如下面这张照片:
它的 Exif 信息为:
1Root:
2 ImageWidth: 4000
3 ImageLength: 3000
4 Make: 'Xiaomi'
5 Model: 'MI CC 9'
6 Orientation: 1
7 XResolution: 72
8 YResolution: 72
9 ResolutionUnit: 2
10 DateTime: '2019:12:16 13:49:17'
11 YCbCrPositioning: 1
12 ExifOffset: 210
13 GPSInfo: 770
14
15Exif:
16 ExposureTime: 1/295 (0.003)
17 FNumber: 179/100 (1.79)
18 PhotographicSensitivity: 112
19 Unknown Tag (0x8895): 0
20 ExifVersion: 48, 50, 50, 48
21 DateTimeOriginal: '2019:12:16 13:49:17'
22 DateTimeDigitized: '2019:12:16 13:49:17'
23 ComponentsConfiguration: 1, 2, 3, 0
24 ShutterSpeedValue: 8202/1000 (8.202)
25 ApertureValue: 167/100 (1.67)
26 ExposureCompensation: 0
27 MaxApertureValue: 167/100 (1.67)
28 MeteringMode: 2
29 LightSource: 0
30 Flash: 16
31 FocalLength: 4740/1000 (4.74)
32 SubSecTime: '646073'
33 SubSecTimeOriginal: '646073'
34 SubSecTimeDigitized: '646073'
35 Unknown Tag (0x9999): '{"mirror":false,"sensor_type":"rear","Hdr":"off","OpMode":36869}'
36 FlashpixVersion: 48, 49, 48, 48
37 ColorSpace: 1
38 ExifImageWidth: 4000
39 ExifImageLength: 3000
40 InteropOffset: 738
41 SensingMethod: 1
42 WhiteBalance: 0
43 FocalLengthIn35mmFormat: 25
44
45Interoperability:
46 InteroperabilityIndex: 'R98'
47 InteroperabilityVersion: 48, 49, 48, 48
48
49Gps:
50 GPSLatitudeRef: 'N'
51 GPSLatitude: 40, 21, 401435/10000 (40.144)
52 GPSLongitudeRef: 'E'
53 GPSLongitude: 116, 1, 14916/10000 (1.492)
54 GPSAltitudeRef: 0
55 GPSAltitude: 722801/1000 (722.801)
56 GPSTimeStamp: 5, 49, 12
57 GPSProcessingMethod: 'GPS'
58 GPSDateStamp: '2019:12:16'
我们可以看到该照片是在 2019-12-16 13:49:17 通过小米 CC 9 拍摄,并且可以看到经纬度海拔等信息。
如果没有去除 Exif 里的敏感信息,那么发布到网络上后,任何人都可以查看 Exif 信息,在某些情况下会造成隐私泄露。
Java 操作 Exif
我们可通过 Apache Commons Imaging 来读取和写入 Exif 元数据。
-
引入依赖 commons-imaging
1<dependency> 2 <groupId>org.apache.commons</groupId> 3 <artifactId>commons-imaging</artifactId> 4 <version>1.0-alpha1</version> 5</dependency>
-
读取 Exif
1final ImageMetadata metadata = Imaging.getMetadata(bytes); 2final JpegImageMetadata jpegMetadata = (JpegImageMetadata) metadata; 3final TiffImageMetadata exif = jpegMetadata.getExif();
-
写入 Exif
1final TiffOutputSet outputSet = exif.getOutputSet(); 2new ExifRewriter().updateExifMetadataLossless(bytes, os, outputSet);
Java 删除敏感 Exif
这里提供两种删除 Exif 信息的方法,第一种比较暴力,通过图片字节数组中 Exif 标识开头和结尾来完整擦除整个 Exif 段:
1public static byte[] removeExif(final byte[] bytes) {
2 try {
3 final byte b0 = bytes[0];
4 final byte b1 = bytes[1];
5 if (-1 != b0 || -40 != b1) { // FF D8
6 return bytes; // not jpeg
7 }
8
9 if (-1 != bytes[2] || -31 != bytes[3]) { // exif seg: FF E1
10 return bytes; // no exif
11 }
12
13 String len0 = Integer.toHexString(bytes[4]);
14 if (2 > len0.length()) {
15 len0 = "0" + len0;
16 } else {
17 len0 = len0.substring(len0.length() - 2);
18 }
19 String len1 = Integer.toHexString(bytes[5]);
20 if (2 > len1.length()) {
21 len1 = "0" + len1;
22 } else {
23 len1 = len1.substring(len1.length() - 2);
24 }
25 final String lenStr = len0 + "" + len1;
26 final int len = Integer.parseInt(lenStr, 16);
27 final byte[] ret = new byte[bytes.length - len - 4 - 2];
28 ret[0] = -1;
29 ret[1] = -40;
30 System.arraycopy(bytes, 4 + len, ret, 2, ret.length - 2);
31 return ret;
32 } catch (final Exception e) {
33 LOGGER.log(Level.ERROR, "Removes Exif failed", e);
34 return bytes;
35 }
36}
该方法的优点是性能很好,但缺点就是会擦除 Exif 中有用的信息(比如方向 Orientation
,如果没有该字段,那么浏览器就无法自动旋转图片为正常方向了)。
另一个方法是通过上面介绍的 commons-imaging 来删除,这样可以保留想要的字段:
1public static byte[] removeExif(final byte[] bytes) throws Exception {
2 try (final ByteArrayOutputStream os = new ByteArrayOutputStream()) {
3 TiffOutputSet outputSet = null;
4 final ImageMetadata metadata = Imaging.getMetadata(bytes);
5 if (!(metadata instanceof JpegImageMetadata)) {
6 return bytes;
7 }
8
9 final JpegImageMetadata jpegMetadata = (JpegImageMetadata) metadata;
10 final TiffImageMetadata exif = jpegMetadata.getExif();
11 if (null != exif) {
12 outputSet = exif.getOutputSet();
13 }
14
15 if (null == outputSet) {
16 return bytes;
17 }
18
19 final List<TiffOutputDirectory> directories = outputSet.getDirectories();
20 for (final TiffOutputDirectory directory : directories) {
21 final List<TiffOutputField> fields = directory.getFields();
22 for (final TiffOutputField field : fields) {
23 if (!StringUtils.equalsIgnoreCase("Orientation", field.tagInfo.name)) {
24 outputSet.removeField(field.tagInfo);
25 }
26 }
27 }
28
29 new ExifRewriter().updateExifMetadataLossless(bytes, os, outputSet);
30 return os.toByteArray();
31 }
32}