通过手机相机或者数码相机拍摄的照片都带有 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 信息,在某些情况下会造成隐私泄露。
我们可通过 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);
这里提供两种删除 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 final JpegImageMetadata jpegMetadata = (JpegImageMetadata) metadata; 6 if (null != jpegMetadata) { 7 final TiffImageMetadata exif = jpegMetadata.getExif(); 8 if (null != exif) { 9 outputSet = exif.getOutputSet(); 10 } 11 } 12 13 if (null == outputSet) { 14 return bytes; 15 } 16 17 final List<TiffOutputDirectory> directories = outputSet.getDirectories(); 18 for (final TiffOutputDirectory directory : directories) { 19 final List<TiffOutputField> fields = directory.getFields(); 20 for (final TiffOutputField field : fields) { 21 if (!StringUtils.equalsIgnoreCase("Orientation", field.tagInfo.name)) { 22 outputSet.removeField(field.tagInfo); 23 } 24 } 25 } 26 27 new ExifRewriter().updateExifMetadataLossless(bytes, os, outputSet); 28 return os.toByteArray(); 29 } 30}