上週因須要編輯了下PDF,用了一兩個試用軟件,感受文字版的PDF仍是挺好編輯的。想要研究一下PDF格式。php
從前輩的文章和書籍瞭解到html
PDF文件是一種文本和二進制混排的格式,二進制的內容來自於三個方面:一、圖片;二、字體;三、壓縮後的Post Script。python
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個複合對象app
基本對象:
Boolean values 布爾值,true
和 false
。編輯器
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 文件由對象圖組成, 間接引用構成它們之間的連接。
圖1 《PDF explained》文件結構->對象
PDF由四部分組成
PDF 處理流程
用一個實例來研究下結構。
爲了研究精簡的PDF文檔結構,新建了一個PDF,內容就是Hello, PDF.
。
圖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的字節。
文件正文由一系列對象組成(上節有介紹),這裏有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
交叉引用表格式:
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對象開始 <</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 對象的開始位置。
圖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'\r\n') try: unzip_data = zlib.decompress(s) stream = unzip_data.decode('UTF-8') print(stream) except Exception as e: print(e)
上面的代碼部分,從文件讀取二進制數據,經過正則能夠拿到流二進制數據,去掉頭尾的\r\n
,十六進制爲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'\r\n') print(s)
匹配到三條流數據:(對應的對象ID分別是7 9 10
)
#(這裏輸出的是7 0 obj對象的流數據) b'x\x9c+\xe4r\n\xe12P\x00\xc1\xa2t\x05\x08#\xc8\x1dH\x94+\xe8\xbbE\xb8\x19*\x18\x1a)\x84\xa4)\x18\x82% dH\xae\x82\xa1\xa5\x9e\xb1\x85\xa1\x82\x85\xa1\x91\x9e\xb1\x99\x99B\x88K\xb4\x86GjNN\xbe\x8eB\x80\x8b\x9b\x9efl\x88\x17\x97k\x08W \x17\x00\xad\x10\x14\x84' #(這裏輸出的是9 0 obj對象的流數據) b"x\x9cm\x8f\xcdJ\xc3P\x10\x85\x97\xbe\xc6\xec\x9a vn~L\xa3\x94@m-\x8a\x08\xc1\x16\\\x88\x8bk26\x17\xea\x8c\xdcL\xfcy\x93>\x9eO\xa2&\x16]\xb9;\x07\xce\xe1\x9c/\x06\x03\t$\x13\xc8 \xcas\x98Nq\xfd\xfeLX\xda\r\xb5x\xe5\xea\xf6\x0e\xd2>r\x03\xf78\x97\x8e\x15\xa2\xa2\xe8C\xd7R/\xacR\xb08\x8dMtb2\x93FI\x9a\x1d'\x87&\x1f\x193\n\xb1\xf4Rw\x15\xf9`)oN\xa1l,\xab<\xc1\x11\xec\xfdJ\x1e\xf5\xd5z\x82K\xae\xc6!\xae\x9dn)\xf8\xfc\xa2\x8f\xe6`\xb7\x0bq\xd6i#>\x10v\xc2!\xce=Y\xed\xd5?\x8bq\xfe\xbb\xf8s\xeb\xcc\xb6\xb4\x14V\xbc\xa0\xed\x0b\xa9\xab,\x9es%\xb5\xe3\r\xde:\x9eq\xeb\xfe\xfc\xaa{\xd0\x01u\xe0\x8d\xf6\xd4C\xb5(\xbe\x01\x00\xf9V\xa3" #(這裏輸出的是10 0 obj對象的流數據) b"x\x9ccb\x00\x81\xff\xff\x99\x18\x81\x94 ##\x98\xfe\xc1\xc0\xc0\x04\x16g`d\xfa\x0f$=\x19\xfe\x83\xe9u@q\x90\x04'\x90\x02\xf1\x9f0\xfc\x03q\x19\xe7B\xd4\xb3l\x80\xd0\x8c.\x0c\x0c\x00\x167\x0b\x9c"
若是沒有從文件二進制中搜索或者想手動看特定的二進制流,就能夠先複製十六進制數據以後轉二進制。直接複製到代碼中實際上是十六進制的字符串,轉二進制數據能夠用 binascii
模塊的 a2b_hex()
# 十六進制字符串轉二進制 stream=b'789C63620081FFFF9918819420232398FEC1C0C00416676064FA0F243D19FE83E97540719004279002F19F30FC037119E742D4B36C80D08C2E0C0C0016370B9C' # 10 0 obj中流的十六進制數據 s2=binascii.a2b_hex(stream) print(s2)
獲得的二進制數據,和上面正則截取10 0 obj
中的二進制流數據的是同樣的
b"x\x9ccb\x00\x81\xff\xff\x99\x18\x81\x94 ##\x98\xfe\xc1\xc0\xc0\x04\x16g`d\xfa\x0f$=\x19\xfe\x83\xe9u@q\x90\x04'\x90\x02\xf1\x9f0\xfc\x03q\x19\xe7B\xd4\xb3l\x80\xd0\x8c.\x0c\x0c\x00\x167\x0b\x9c"
直接用zlib模塊進行解壓:
unzip_data = zlib.decompress(s) print(unzip_data)
解壓後的三個對象數據爲:
b'q\nBT\n0 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\nET\nQ\n' 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(\xfe\xffe\xe0h\x07\x98\x98)/Author(onion)/CreationDate(D:20190604134628+08'00')>><</BaseFont/Helvetica/Encoding/WinAnsiEncoding/Subtype/Type1/Type/Font>>" b'\x02\x00\x00\x00\x00\x00\xff\xff\x02\x01\x00\x00\x00\x11\x01\x01\x02\x01\x00\x00\x00\xf8\x00\x00\x02\x00\x00\x00\x00\x00\x00\x01\x02\xff\x00\x00\x00I\x00\xff\x02\xff\x00\x00\x00\xae\x00\x00\x02\x02\x00\x00\x00\t\x00\x02\x02\xff\x00\x00\x00\xe4\x00\xfe\x02\x00\x00\x00\x01\x9d\x00\x00\x02\x00\x00\x00\x04\xb0\x00\x00\x02\x00\x00\x00\x01D\x00\x00'
解壓很順利,再進行解碼字符串操做:然而三個對象反應各不同,各個分析。
對於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
,/Type
是ObjStm
應該指的對象流數據,解碼的時候有報錯
'utf-8' codec can't decode byte 0xfe in position 140: invalid start byte
對應解壓數據查看,發現0xfe
出如今Title(\xfe\xffe\xe0h\x07\x98\x98)
, 報錯緣由就在這:Title
裏面的字符串不能用UTF-8編碼正確解析,不知道是什麼編碼。因爲對這個對象流數據不太瞭解,先暫時擱置,之後再來回顧。
對於10 0 obj
數據,這個數據也不能正確解碼,是由於除了/filter
參數,還有個 /DecodeParms
參數,這個是指數據加了PNG壓縮算法的預處理(過濾)。接下來就來看看這個。
本小節圖片和算法部分參考:PDF 參照流/交叉引用流對象(cross-reference stream)的解析方法
交叉引用流對象一般在存儲以前都會進行壓縮,而爲了提升壓縮率,會進行數據的預處理,這個預處理就稱爲過濾(filter)。讀取時再進行反向的處理。
PNG規範中的過濾算法章節有說:
本章介紹可在壓縮以前應用的過濾器算法。這些濾波器的目的是準備圖像數據以實現最佳壓縮。
雖然PDF不是圖片,可是爲實現最佳壓縮,能夠先使用過濾算法。
圖6 帶有PNG壓縮算法的交叉引用流的壓縮和過濾
因此,直接用zlib的zip解壓算法解壓後,還須要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 <= width: 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()
方法是爲了方便查看十六進制以圖片的行列排列的格式。
圖7 反過濾 xref 數據
看圖7,第一個框中是解壓後的數據,第二個框中是反過濾的數據,即xref原始數據。
因爲不知道解析的xref數據格式對不對,還一直去轉,期待轉成7 0 obj
的那種可視化的字符格式,最後在 PDF 文件格式 文檔中看到示例才反應過來,xref 流數據就是這樣的格式。
圖8 xref數據分析示例
圖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 的起始位置
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(\xfe\xffe\xe0h\x07\x98\x98)/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(\xfe\xffe\xe0h\x07\x98\x98)/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(\xfe\xffe\xe0h\x07\x98\x98)/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(\xfe\xffe\xe0h\x07\x98\x98)/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的相遇,到這裏就要說再見了。英語又鎖死了這條科技樹了。原本想要作一個簡單編輯的,發現展現還有很長的路走(圖像的繪製,字體加載)等等。未完待續