D 的个人博客

全职做开源,自由职业者

  menu

Java 提取和删除照片图片 Exif GPS 等隐私信息

照片图片 Exif

通过手机相机或者数码相机拍摄的照片都带有 Exif 元数据信息,比如下面这张照片:

IMG20191216134916.jpg

它的 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 元数据。

  1. 引入依赖 commons-imaging

    1<dependency>
    2  <groupId>org.apache.commons</groupId>
    3  <artifactId>commons-imaging</artifactId>
    4  <version>1.0-alpha1</version>
    5</dependency>
    
  2. 读取 Exif

    1final ImageMetadata metadata = Imaging.getMetadata(bytes);
    2final JpegImageMetadata jpegMetadata = (JpegImageMetadata) metadata;
    3final TiffImageMetadata exif = jpegMetadata.getExif();
    
  3. 写入 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}