zoukankan      html  css  js  c++  java
  • PDF格式简单分析

    上周因需要编辑了下PDF,用了一两个试用软件,感觉文字版的PDF还是挺好编辑的。想要研究一下PDF格式。

    0. 站在前辈的肩膀上

    从前辈的文章和书籍了解到

    • PDF文件是一种文本和二进制混排的格式,二进制的内容来自于三个方面:1、图片;2、字体;3、压缩后的Post Script。

    • PDF文件正文由一系列对象组成, 每个对象前面都有一个对象编号(唯一)、生成号和一行上的 obj 关键字, 后面跟另一行的 endobj 关键字。例如:

      1 0 obj
      <<
      /Kids [2 0 R]
      /Count 1
      /Type /Pages
      >>
      endobj
      

      在这里, 对象编号为 1, 生成号为 0 (几乎总是)。对象1的内容位于两行之间 1 0 obj 和 endobj 之间。在这种情况下, 它是字典 <</Kids [2 0 R] /Count 1 /Type /Pages >>

    • 对象:PDF对象包括5个基本对象以及3个复合对象

      • 基本对象:
        Boolean values 布尔值,truefalse

        Integers and real numbers 数值,包含整型和浮点型,例如 42和3.1415。

        Strings 字符串,文字字符串包含在圆括号()内,十六进制字符串包含在单尖括号<>内。

        Names 名称,由 /+字符串 组成,相同的名字表示相同的对象。

        Null 空对象,用关键字null表示。

        上面是基础对象,可组合为复合对象:

      • 复合对象

        Array 数组,包含其他对象的有序集合,包含在方括号[]内,元素可以是除了Stream类型外的所有类型,如 [/xx false 1 (onion)] 包含了四种类型,注:用空格分隔。

        Dictionary 字典,包含一个无序键值对的集合,包含在双尖括号<< >>内。两个元素是一对,键是对象的名称,值是除了Stream外的所有类型。例如, <</Contents 4 0 R /Resources 5 0 R>> , 它将 /Contents 映射到间接引用 4 0 R ,以及 /Resources 映射到间接引用5 0 R

        Stream 流,包含二进制数据,以及描述数据属性(如长度和压缩参数)的字典数据。PDF Stream由一个字典和一个字节流(流用于存储图像、字体等)组成,字典中定义了流的参数。字典中如果有/Filter键,表示指定的过滤器类型,压缩过滤器 FlateDecode 最为常用。(下一节会看到没有压缩过滤器,采用XML的流对象)。

        补:Object Object 流对象类型,PDF 1.5中引入。以及其他的类型,以后遇到再补充。

      • 间接引用

        除了基本对象和复合对象,还有一种将对象链接在一起的方法:

        Indirect reference 间接引用,它形成从一个对象到另一个对象的链接。

      PDF 文件由对象图组成, 间接引用构成它们之间的链接。

    1559699185089

    图1 《PDF explained》文件结构->对象

    • PDF由四部分组成

      1559629734691

    • PDF 处理流程

    1559629349714

    用一个实例来研究下结构。

    1. Hello, PDF

    为了研究精简的PDF文档结构,新建了一个PDF,内容就是Hello, PDF.

    1559629210317

    图4 hello.pdf,一个 hello world级别的 PDF

    用编辑器打开,查看文本,流对象不能查看,用...替换掉。文本为下面部分

    %PDF-1.7
    %����
    1 0 obj
    <</Pages 2 0 R /Type/Catalog/Metadata 8 0 R >>
    endobj
    4 0 obj
    <</Resources<</Font<</FXF1 6 0 R >>>>/MediaBox[ 0 0 595.28 841.89]/Contents 7 0 R /Parent 2 0 R /Type/Page/CropBox[ 0 0 595.28 841.89]>>
    endobj
    7 0 obj
    <</Length 86/Filter/FlateDecode>>stream
    ...
    endstream
    endobj
    8 0 obj
    <</Length 865/Type/Metadata/Subtype/XML>>stream
    ...
    endstream
    endobj
    9 0 obj
    <</Type /ObjStm /N 3/First 15/Length 224/Filter /FlateDecode>>stream
    ...
    endstream
    endobj
    10 0 obj
    <</Type /XRef/W[1 4 2]/Index[0 11]/Size 11/Filter /FlateDecode/DecodeParms<</Columns 7/Predictor 12>>/Length 64
    /Root 1 0 R
    /Info 3 0 R
    /ID[<5E1FEDA5466E60C6D70D3004F5E43166><5E1FEDA5466E60C6D70D3004F5E43166>]>>stream
    ...
    endstream
    endobj
    
    startxref
    1662
    %%EOF
    

    简单分析:

    第一行 %PDF-1.7%符号表示一个标题行,这里给出了文件的PDF版本号 1.7。

    第二行 %����%表示另一个标题行,乱码内容为大于127字节的二进制数据。由于PDF文件几乎总是包含二进制数据,因此如果更改行结尾(例如,如果文件通过FTP以文本模式传输),它们可能会损坏。 为了允许传统文件传输程序确定文件是二进制文件,通常在标头中包含一些字符代码高于127的字节。

    Body

    文件正文由一系列对象组成(上节有介绍),这里有6个对象,分别是

    1 0 obj ... endobj
    4 0 obj ... endobj
    7 0 obj ... endobj
    8 0 obj ... endobj
    9 0 obj ... endobj
    10 0 obj ... endobj
    

    Cross-Reference Table

    交叉引用表格式:

    xref                    # 标识交叉引用表开始
    0 14                    # 说明下面对象编号是从0开始,总共有14个对象, 从 0 到 13
    0000000000 65535 f      # 第0个对象,规定生成号为65535,f 表示 free entry,对象不存在或者删除
    0000003079 00000 n      # 第1个对象,偏移地址为3079,生成号为0表示未被修改过, n 表示 in use
    

    从 PDF 1.5 开始, 引入了一种新的机制, 通过允许将许多对象放入单个对象流 (整个流被压缩) 来进一步压缩 PDF 文件。同时, 引入了一种引用这些流中对象的新机制--交叉引用流(cross-reference streams)。

    在本次测试中的PDF不存在关键字 xref开头的引用,只有基于流的引用,10 0 obj这个对象可能就是了。

    交叉引用流格式是这样的: (现在只接触过 类型为 /XRef/ObjStm 的交叉引用流对象)

    x 0 obj 
    <</Type /XRef ...>>stream
    ... 
    endstream 
    endobj
    
    x 0 obj 
    <</Type /ObjStm ...>>stream
    ... 
    endstream 
    endobj
    

    Trailer

    测试文本中没有看到Trailer关键字,类似

    trailer                 # 标识文件尾trailer对象开始
    <</Root 13 0 R          # 表明根对象的对象号为13,即交叉表中的最后一个对象
    /ID [<4E76CDCEDB1E2EC4AC47475DB4EE376E> <C8B1AEBC2C6615E39860F1C150A2847C>]
    /Size 14                # 表明PDF文件的对象数目
    /Info 8 0 R>>
    

    以后遇到了再分析。

    倒数第三行的 startxref,标明了交叉引用表的偏移地址,下面的数字1662代表了偏移量(相对于文件开始)。因为一个文档中可以有多个xref,所以这里要指明要从哪个xref开始进行解析这个文件。

    1662的十六进制为67E, 将文件以十六进制格式打开,刚好是10 0 obj那个 xref 对象的开始位置。

    1559714932549

    图5 16进制查看起始位置的对象

    最后一行 %%EOF, 标识 PDF 文件结尾。

    字典数据

    从起始位置的对象10 0 obj分析。先看其中的字典数据,先格式化一下,按照我的理解简单标注

    <<
    /Type /XRef     # 类型为xref,表示此对象是基于流的交叉引用表
    /W[1 4 2]       # W的值为数组 [1 4 2]
    /Index[0 11]    # Index 值为 [0 11]
    /Size 11        # 文件数目?
    /Filter /FlateDecode    # 指定压缩算法,RFC1950,即ZLIB
    /DecodeParms <</Columns 7/Predictor 12>>    
    /Length 64  # stream 的数据长度
    /Root 1 0 R
    /Info 3 0 R
    /ID[<5E1FEDA5466E60C6D70D3004F5E43166><5E1FEDA5466E60C6D70D3004F5E43166>]   # ID为数组,数组值为两个十六进制字符串,见到了<>(尖括号)内的的十六进制
    >>
    

    解析流

    10 0 obj下的stream二进制数据,用zlib解压,没出来想要的结果。学习一下如何正确的解析流数据。

    找到一串代码,修改后:

    import re
    import zlib
    
    pdf = open("hello.pdf", "rb").read()
    stream = re.compile(b'.*?FlateDecode.*?stream(.*?)endstream', re.S)
    
    for s in re.findall(stream,pdf):
        s = s.strip(b'
    ')
        try:
            unzip_data = zlib.decompress(s)
            stream = unzip_data.decode('UTF-8')
            print(stream)
        except Exception as e:
            print(e)
    
    二进制的流数据

    上面的代码部分,从文件读取二进制数据,通过正则可以拿到流二进制数据,去掉头尾的 ,十六进制为0D0A

    pdf = open("some_doc.pdf", "rb").read()
    stream = re.compile(b'.*?FlateDecode.*?stream(.*?)endstream', re.S)
    
    for s in re.findall(stream,pdf):
        s = s.strip(b'
    ')
        print(s)
    

    匹配到三条流数据:(对应的对象ID分别是7 9 10

    #(这里输出的是7 0 obj对象的流数据)
    b'xx9c+xe4r
    xe12Px00xc1xa2tx05x08#xc8x1dHx94+xe8xbbExb8x19*x18x1a)x84xa4)x18x82% dHxaex82xa1xa5x9exb1x85xa1x82x85xa1x91x9exb1x99x99Bx88Kxb4x86GjNNxbex8eBx80x8bx9bx9eflx88x17x97kx08W x17x00xadx10x14x84'
    
    #(这里输出的是9 0 obj对象的流数据)
    b"xx9cmx8fxcdJxc3Px10x85x97xbexc6xecx9a vn~Lxa3x94@m-x8ax08xc1x16\x88x8bk26x17xeax8cxdcLxfcyx93>x9eOxa2&x16]xb9;x07xcexe1x9c/x06x03	$x13xc8 xcasx98NqxfdxfeLXxda
    xb5xxe5xeaxf6x0exd2>rx03xf78x97x8ex15xa2xa2xe8Cxd7R/xacRxb08x8dMtb2x93FIx9ax1d'x87&x1fx193
    xb1xf4Rwx15xf9`)oNxa1l,xab<xc1x11xecxfdJx1exf5xd5zx82Kxaexc6!xaex9dn)xf8xfcxa2x8fxe6`xb7x0bqxd6i#>x10vxc2!xce=Yxedxd5?x8bqxfexbbxf8sxebxccxb6xb4x14Vxbcxa0xedx0bxa9xab,x9es%xb5xe3
    xde:x9eqxebxfexfcxaa{xd0x01uxe0x8dxf6xd4Cxb5(xbex01x00xf9Vxa3"
    
    #(这里输出的是10 0 obj对象的流数据)
    b"xx9ccbx00x81xffxffx99x18x81x94 ##x98xfexc1xc0xc0x04x16g`dxfax0f$=x19xfex83xe9u@qx90x04'x90x02xf1x9f0xfcx03qx19xe7Bxd4xb3lx80xd0x8c.x0cx0cx00x167x0bx9c"
    

    如果没有从文件二进制中搜索或者想手动看特定的二进制流,就可以先复制十六进制数据之后转二进制。直接复制到代码中其实是十六进制的字符串,转二进制数据可以用 binascii 模块的 a2b_hex()

    # 十六进制字符串转二进制
    stream=b'789C63620081FFFF9918819420232398FEC1C0C00416676064FA0F243D19FE83E97540719004279002F19F30FC037119E742D4B36C80D08C2E0C0C0016370B9C' # 10 0 obj中流的十六进制数据
    s2=binascii.a2b_hex(stream)
    print(s2)
    

    得到的二进制数据,和上面正则截取10 0 obj中的二进制流数据的是一样的

    b"xx9ccbx00x81xffxffx99x18x81x94 ##x98xfexc1xc0xc0x04x16g`dxfax0f$=x19xfex83xe9u@qx90x04'x90x02xf1x9f0xfcx03qx19xe7Bxd4xb3lx80xd0x8c.x0cx0cx00x167x0bx9c"
    
    解压

    直接用zlib模块进行解压:

    unzip_data = zlib.decompress(s)
    print(unzip_data)
    

    解压后的三个对象数据为:

    b'q
    BT
    0 0 0 rg 0 0 0 RG 0 w /FXF1 12 Tf 1 0 0 1 0 0 Tm 19.381 812.366 TD[(Hello, PDF.)]TJ
    ET
    Q
    '
    b"2 0 3 37 6 188 <</Type/Pages/Kids[ 4 0 R ]/Count 1>><</ModDate(D:20190604134653+08'00')/Producer(Foxit Phantom - Foxit Software Inc.)/Title(xfexffexe0hx07x98x98)/Author(onion)/CreationDate(D:20190604134628+08'00')>><</BaseFont/Helvetica/Encoding/WinAnsiEncoding/Subtype/Type1/Type/Font>>"
    b'x02x00x00x00x00x00xffxffx02x01x00x00x00x11x01x01x02x01x00x00x00xf8x00x00x02x00x00x00x00x00x00x01x02xffx00x00x00Ix00xffx02xffx00x00x00xaex00x00x02x02x00x00x00	x00x02x02xffx00x00x00xe4x00xfex02x00x00x00x01x9dx00x00x02x00x00x00x04xb0x00x00x02x00x00x00x01Dx00x00'
    

    解压很顺利,再进行解码字符串操作:然而三个对象反应各不一样,各个分析。

    对于7 0 obj数据,回顾下字典指定的参数 , 除了压缩参数/Filter/FlateDecode和长度之外,再无其他,直接解压解码正确,结果是:(格式就是这样,具体含义后面再做分析)

    q
    BT
    0 0 0 rg 0 0 0 RG 0 w /FXF1 12 Tf 1 0 0 1 0 0 Tm 19.381 812.366 TD[(Hello, PDF.)]TJ
    ET
    Q
    

    对于9 0 obj数据,字典数据参数有/Type /ObjStm /N 3/First 15/Length 224/Filter /FlateDecode/TypeObjStm应该指的对象流数据,解码的时候有报错

    'utf-8' codec can't decode byte 0xfe in position 140: invalid start byte
    

    对应解压数据查看,发现0xfe出现在Title(xfexffexe0hx07x98x98), 报错原因就在这:Title里面的字符串不能用UTF-8编码正确解析,不知道是什么编码。由于对这个对象流数据不太了解,先暂时搁置,以后再来回顾。

    对于10 0 obj数据,这个数据也不能正确解码,是因为除了/filter 参数,还有个 /DecodeParms 参数,这个是指数据加了PNG压缩算法的预处理(过滤)。接下来就来看看这个。

    加过滤的流的解析

    本小节图片和算法部分参考:PDF 参照流/交叉引用流对象(cross-reference stream)的解析方法

    交叉引用流对象通常在存储之前都会进行压缩,而为了提高压缩率,会进行数据的预处理,这个预处理就称为过滤(filter)。读取时再进行反向的处理。

    PNG规范中的过滤算法章节有说:

    本章介绍可在压缩之前应用的过滤器算法。这些滤波器的目的是准备图像数据以实现最佳压缩。

    虽然PDF不是图片,但是为实现最佳压缩,可以先使用过滤算法。

    1559725319805

    图6 带有PNG压缩算法的交叉引用流的压缩和过滤

    所以,直接用zlib的zip解压算法解压后,还需要PNG过滤反转。

    PNG过滤算法反转

    要反转解压缩后Up()过滤器的效果,请输出以下值:

    Up(x)+ Prior(x)

    (计算的模256),其中Prior()指的是先前扫描线的解码字节。

    反转方法封装:

    def show_hex_format(data, rows, columns):
        """ 展示十六进制格式 """
        for i in range(rows):
            for j in range(columns):
                print("%02x" % (data[i*columns+j]), end=" ")
            print()
    
    
    def filter_up_reverse(stream_data, columns, colors=1, bitsPerComponent=8):
        """ PNG过滤器UP方法,反转方法 """
        stream_data = bytearray(stream_data)
        xref_data = []
    
        data_len = len(stream_data)
        width = columns*colors*bitsPerComponent//8
        rows = data_len//(width+1)
    
        show_hex_format(stream_data, rows, width+1)
    
        cursor = 1
    
        # 第一行处理,跳过
        while cursor <= 
            xref_data.append(stream_data[cursor])
            cursor += 1
    
        for i in range(1, rows):
            filter_type = stream_data[cursor]
            cursor += 1
            assert(filter_type == 2)
    
            for j in range(width):
                t = (stream_data[cursor]+stream_data[cursor-width-1]) % 256
                stream_data[cursor] = t
                xref_data.append(t)
                cursor += 1
    
        print("xref stream data:")
        show_hex_format(xref_data, rows, width)
    

    对于10 0 obj对象,使用filter_up_reverse(stream_unzip_data, 7) 调用,列宽为7来自/DecodeParms <</Columns 7/Predictor 12>> Columns属性,另外:Predictor属性代表过滤器采用UP方法,取上行的原始数据。show_hex_format()方法是为了方便查看十六进制以图片的行列排列的格式。

    1560135051210

    图7 反过滤 xref 数据

    看图7,第一个框中是解压后的数据,第二个框中是反过滤的数据,即xref原始数据。

    xref 交叉引用流数据分析

    由于不知道解析的xref数据格式对不对,还一直去转,期待转成7 0 obj的那种可视化的字符格式,最后在 PDF 文件格式 文档中看到示例才反应过来,xref 流数据就是这样的格式。

    1560135563147

    图8 xref数据分析示例

    1560137471619

    图9 交叉引用流 (xref strem) 的属性

    三个位域的字节划分就来子属性W,W[1 4 2], 分别是第一个字节,中间四个字节,后两个字节。(1+4+2也对应到列宽为7字节)

    分析10 0 obj的数据:

    00 00 00 00 00 ff ff	# xref 起始标识
    01 00 00 00 11 00 00	# 直接对象,偏移量为0x11,查看该地址,对应 1 0 obj (对象ID:1)的起始位置
    02 00 00 00 09 00 00	# 间接对象,对象id为9,索引0
    02 00 00 00 09 00 01	# 间接对象,对象id为9,索引1
    01 00 00 00 52 00 00	# 直接对象,偏移量为0x52,查看该地址,对应 4 0 obj 的起始位置
    00 00 00 00 00 00 00
    02 00 00 00 09 00 02	# 间接对象,对象id为9,索引2
    01 00 00 00 ed 00 00	# 直接对象,偏移量为0xed,查看该地址,对应 7 0 obj 的起始位置
    01 00 00 01 8a 00 00	# 直接对象,偏移量为0x18a,查看该地址,对应 8 0 obj 的起始位置
    01 00 00 05 3a 00 00	# 直接对象,偏移量为0x53a,查看该地址,对应 9 0 obj 的起始位置
    01 00 00 06 7e 00 00	# 直接对象,偏移量为0x67e,查看该地址,对应 10 0 obj 的起始位置
    

    objStm 对象流数据分析

    9 0 obj
    <</Type /ObjStm /N 3/First 15/Length 224/Filter /FlateDecode>>stream
    2 0 3 37 6 188 <</Type/Pages/Kids[ 4 0 R ]/Count 1>><</ModDate(D:20190610144818+08'00')/Producer(Foxit Phantom - Foxit Software Inc.)/Title(xfexffexe0hx07x98x98)/Author(onion)/CreationDate(D:20190610144753+08'00')>><</BaseFont/Helvetica/Encoding/WinAnsiEncoding/Subtype/Type1/Type/Font>>
    endstream
    endobj
    

    对于9 0 obj/Type/ObjStm,表示此对象为对象流数据。N表示流中的间接对象数(本例有3个对象),First表示解码流中第一个可压缩对象的偏移量(本例中代表解压数据后的15字节处为第一个对象)。

    流中开始的2 0 3 37 6 188 是键值对表示的索引,键值对分别表示:对象ID和该对象在解码流中的字节偏移量。(例如:3 37 表示 对象3的偏移量为解压后数据的37字节处)

    示例输出:(注:>>> 表示输出的数据)

    print(stream_unzip_data) # 解压后的原始流数据
    >>> b"2 0 3 37 6 188 <</Type/Pages/Kids[ 4 0 R ]/Count 1>><</ModDate(D:20190610144818+08'00')/Producer(Foxit Phantom - Foxit Software Inc.)/Title(xfexffexe0hx07x98x98)/Author(onion)/CreationDate(D:20190610144753+08'00')>><</BaseFont/Helvetica/Encoding/WinAnsiEncoding/Subtype/Type1/Type/Font>>"
    stream_data_r = stream_unzip_data[15:] # 间接对象(所处的位置15)
    print(stream_data_r)
    >>> b"<</Type/Pages/Kids[ 4 0 R ]/Count 1>><</ModDate(D:20190610144818+08'00')/Producer(Foxit Phantom - Foxit Software Inc.)/Title(xfexffexe0hx07x98x98)/Author(onion)/CreationDate(D:20190610144753+08'00')>><</BaseFont/Helvetica/Encoding/WinAnsiEncoding/Subtype/Type1/Type/Font>>"
    
    print("obj id:2 [0-36):", stream_data_r[:37]) # 对象ID:2,从0开始,到第二对象37之前即36
    print("obj id:3 [37-187):",stream_data_r[37:188]) # 对象ID:3
    print("obj id:6 [188-~):",stream_data_r[188:]) # 对象ID:6
    
    >>> obj id:2 [0-36): b'<</Type/Pages/Kids[ 4 0 R ]/Count 1>>'
    >>> obj id:3 [37-187): b"<</ModDate(D:20190610144818+08'00')/Producer(Foxit Phantom - Foxit Software Inc.)/Title(xfexffexe0hx07x98x98)/Author(onion)/CreationDate(D:20190610144753+08'00')>>"
    >>> obj id:6 [188-~): b'<</BaseFont/Helvetica/Encoding/WinAnsiEncoding/Subtype/Type1/Type/Font>>'
    

    最后的三个对象也对应到了上节 xref 数据的 02 00 00 00 09 00 [00,01,02] # 间接对象,对象id为9,索引0,1,2 。终于连起来了。

    分析就到这里了。

    初次与PDF的相遇,到这里就要说再见了。英语又锁死了这条科技树了。本来想要做一个简单编辑的,发现展示还有很长的路走(图像的绘制,字体加载)等等。未完待续

    参考

    1. PDF 文档结构

    2. PDF文件格式的一些研究心得

    3. PDF源文件浅析

    4. C# Parsing 类实现的 PDF 文件分析器

    5. John Whitington.PDF explained.O'Reilly Media (2011)

    6. Decompress FlateDecode Objects in PDF in Python

    7. PDF 参照流/交叉引用流对象(cross-reference stream)的解析方法

    8. PNG-Filters#Filter-type-2-Up

    9. 中华人民共和国国家标准文献管理可移植文档格式第1 部分

    10. PDF Reference, Sixth Edition, version 1.7

    11. PDF 文件格式, 洋文馆

  • 相关阅读:
    LeetCode 453 Minimum Moves to Equal Array Elements
    LeetCode 112 Path Sum
    LeetCode 437 Path Sum III
    LeetCode 263 Ugly Number
    Solutions and Summay for Linked List Naive and Easy Questions
    AWS–Sysops notes
    Linked List
    All About Linked List
    datatable fix error–Invalid JSON response
    [转]反编译c#的相关问题
  • 原文地址:https://www.cnblogs.com/warcraft/p/10998541.html
Copyright © 2011-2022 走看看