起因
早上起来,看到有人问Python获取一张JPG格式图片拍摄的时候的GPS定位的代码。GPS应该说是个敏感的信息,既然有人想读取我们的信息,那么我们至少应该直到我们的敏感信息被保存在了哪里。
研究了一天,四处搜集文档,对着一张JPG格式文件的二进制代码,终于摸到了点门道。结论就是并不是所有的图片都带着GPS等信息,例如我们微信发送图片的时候,如果不发送原图,很多信息都会被抹除(抹除APP1标签,下文有介绍)。
这里顺路推荐一个Linux下查看二进制文件的一个命令行工具:hexedit,Ubuntu下使用命令sudo apt install hexedit就可以安装,虽然没有UltraEdit这种好用,但是对于查看文件也足够了。
写代码,肯定要先直到原理,因此下面需要简单介绍下JPEG这个东西。
JPG 简介
JPEG(Joint Photographic Experts Group,联合图像专家组)标准定义了一套对静态图片进行压缩的算法,用于对图像或者视频进行压缩。JPEG标准定义了四种操作,分别是顺序DCT(sequential Discrete Cosine Transform) 模式、渐进DCT(progressive DCT)模式、无损(Lossless)模式和分层(hierarchical)模式。根据不同模式会对原是图像进行多次扫描,每扫描一次就得到一帧。每一帧前面会添加有一些压缩的参数,例如量化表、霍夫曼编码表等。这套算法可以对图像数据或者视频数据进行压缩,但是却没有定义怎么将这些压缩后的数据通过一个图片格式保存。因此JFIF(JPEG File Interchange Format)就成了一个事实上的标准,它通过标签段的方式,为这些压缩数据提供了额外的信息。
另外还有一种表示JPEG图片的格式是Exif( Exchangeable image file Format),它并不是一个新的标准,而是通过组合已有标准而成的格式。对压缩数据的标准使用的是(ISO/IEC 10918-1),和JFIF的标准一样,只不过增加了一个额外表示信息的标签APP1,这个APP1的格式标准使用的是(TIFF Rev. 6.0)。因此他们俩主要区别就是附加信息的标签不同,JFIF的标签是APP0,而Exif的标签是APP1。而例如拍摄图片的所用的相继型号等信息,就是储存在APP1标签里面。
我们的主要目的是获取图片属性信息,因此我们主要介绍Exif的格式。
Exif图片的主要结构如图所示,被特定的标签被分成了一段一段的数据。所谓标签,就是2个有特定数值的字节,它们对应的名字和数值如图所示。这些标签其实就是一组特定的数值,每个标签占两个字节,其中灰色的是必须有的结构,白色的根据情况有可能没有。
Exif格式中可能存在的标签以及每个标签对应的值和含义如下:
APP1
因为我们关注的重点是APP1标签,所以我们看一下APP1的结构,其他标签等如DQT等示关于压缩数据的,如果需要处理图片内容的可以详细看,这里就忽略了。APP1有两种形式,一种是带缩略图信息的,另一种是不带缩略图信息的,分别如下面图和图所示:
我们已经直到APP1标签的值示0xFFE1
;而Length记录了APP1段的长度,长度不能超过64KB,因为Length占用2个字节记录长度,因此最大子能表示64KB;Exif标示符占6个字节,内容是0x45 0x78 0x69 0x66 0x00 0x00
,也就是Exif
四个字符外加两个空字符;而TIFF头如下图所示:
TIFF头之后就是IFD和IFD的值。0th IFD主要存储主图的信息,1st IFD可能存储着缩略图的信息。
IFD(Image File Directory) 结构
JPEG图片的信息存储分为两部分:IFD和IFD Value。IFD是一个线索,通过这个线索可以找到IFD Value。举个栗子,IFD就是租房中介,IFD Value是房子。中介手上会有所有房子的信息,你找到了中介,就能直到所有房子的数量、户型、地址。
IFD由三部分组成:
- 第一部分:占据2个字节,这两个字节记录一共有多少条信息;
- 根据第一部分记录的信息的数量,每条信占用12个字节,着十二个字节又可以分为四个部分:
1) Tag:2个字节表示这条信息的类型,比如是记录长、宽还是拍摄时间等信息;
2) Type:2个字节表示这些记录存储为二进制的信息怎么翻译,比如是翻译成整数、小数还是字符串等;
3)Count:4个字节表示这个记录的值有多少个,不一定示多少个字节。例如当值的类型示无符号整型的时候,因为一个无符号整型数值占用两字节,Count = 1就表示两个字节;
4) Offset:4个字节表示找到这条记录的偏移地址,以TIFF头为基准。如果记录的数使用着四个字节就装的下,那么Offset记录的就不是地址而是实际的值,如果所用到的字节小于4,那么从最左边的字节开始使用,也就是Offset的低位开始。例如存储一个SHORT类型1的时候,大端格式中着四个字节的内容就是0x0001 0000
,而小端格式就是0x0000 0000 0000 0001
。 - 第三部分占用4个细节,记录的是下一个IFD的偏移地址,如果没有下一个IFD了,这四个字节的值就是0x00000000。
IFD的结构如下:
IFD中Type的含义如下:
0th IFD中Tag的值和其对应意思如下:
可以看到,上面的Tag表格中并为包含GPS信息,因为GPS自己有一个属于自己的IFD,在0th IFD中只有一个指向GPS IFD的指针:
有了上面的铺垫,我们就可以开始编程了。
Coding
下面是我根据我的理解写的代码,额外用到了一个numpy
库,平常工作中用的比较多,更重要的是它能够将图片按照我的想法读取进内存。
代码的思路是是这样:
- 确定这是一张JPG图片:通过SOI标签确定,这个标签在文件的最开头,内容是
0xFFD8
; - 找到APP1标签,理论上APP1标签是要紧跟着SOI标签的,但是我在查看图片二进制内容的时候发现并不是所有的照片都这样,因此使用搜索APP1的方法;
- 找到0th IFD,按照上文中的解释来找;
- 把找到的标签的、TIFF头的地址都记录下来
- 获取所有的IFD;
- 看找到的IFD有没有GPS IFD的指针
- 通过GPS IFD的指针找到GPS IFD;
- 读取GPS信息
下面是代码实现,另外源代码在本人Github地址为:https://github.com/zmychou/jpg-info-extractor
时间很晚了,就先草草收场了。
import numpy as np
IMAGE = []
markers = {
'APP1': [0xFF, 0xE1],
'SOI': [0xFF, 0xD8]
}
class APP1(object):
attributes_name = {
271: 'Manufacturer',
272: 'Model',
306: 'Last Modify',