PDF格式原理学习
1、PDF的歷史
PDF的基本格式:
- PDF是Portable Document Format 的縮寫,可翻譯為“便攜文件格式”,由Adobe System Incorporated 公司在1992年發明。
- PDF文件是一種編程形式的文檔格式,它所有顯示的內容,都是通過相應的操作符進行繪制的。
- PDF基本顯示單元包括:文字,圖片,矢量圖,圖片
- PDF擴展單元包括:水印,電子署名,注釋,表單,多媒體,3D
- PDF動作單元:書簽,超鏈接(擁有動作的單元有很多個,包括電子署名,多媒體等等)
PDF的優點:
- 一致性:在所有可以打開PDF的機器上,展示的效果是完全一致,不會出現段落錯亂、文字亂碼這些排版問題。尤其是文檔中,本身可以嵌入字體,避免了客戶端沒有對應字體,而導致文字顯示不一致的問題。所以,在印刷行業,絕大多數用的都是PDF格式。
- 不易修改:用過PDF文件的人,都會知道,對已經保存之后的PDF文件,想要進行重新排版,基本上就不可能的,這就保證了從資料源發往外界的資料,不容易被篡改。
- 安全性:PDF文檔可以進行加密,包括以下幾種加密形式:文檔打開密碼,文檔權限密碼,文檔證書密碼,加密的方法包括:RC4,AES,通過加密這種形式,可以達到資料防擴散等目的。
- 不失真:PDF文件中,使用了矢量圖,在文件瀏覽時,無論放大多少倍,都不會導致使用矢量圖繪制的文字,圖案的失真。
- 支持多種壓縮方式:為了減少PDF文件的size,PDF格式支持各種壓縮方式:asciihex,ascii85,lzw,runlength,ccitt,jbig2,jpeg(DCT),jpeg2000(jpx)
- 支持多種印刷標準:支持PDF-A,PDF-X
2、PDF的格式
理解PDF格式可以從四個方面下手——Objects(對象)、File structure(物理文件結構)、Document structure(邏輯文件結構)、Content streams(內容流)。
1、物理文件結構
- 整體上分為文件頭(Header)、對象集合(Body)、交叉引用表(Xref table)、文件尾(Trailer)四個部分,結構如圖。修改過的PDF結構會有部分變化。
未經修改
經過修改
(1)文件頭 - 文件頭是PDF文件的第一行,格式如下:%PDF-1.7
- 這是個固定格式,表示這個PDF文件遵循的PDF規范版本,解析PDF的時候盡量支持高版本的規范,以保證支持大多數工具生成的PDF文件。1.7版本支持1.0-1.7之間的所有版本。
(2)對象集合 - 這是一個PDF文件最重要的部分,文件中用到的所有對象,包括文本、圖象、音樂、視頻、字體、超連接、加密信息、文檔結構信息等等,都在這里定義。格式如下:2 0 obj... end obj
- 一個對象的定義包含4個部分:
前面的2是對象序號,其用來唯一標記一個對象;
0是生成號,按照PDF規范,如果一個PDF文件被修改,那這個數字是累加的,它和對象序號一起標記是原始對象還是修改后的對象,但是實際開發中,很少有用這種方式修改PDF的,都是重新編排對象號;
obj和endobj是對象的定義范圍,可以抽象的理解為這就是一個左括號和右括號;
省略號部分是PDF規定的任意合法對象。 - 可以通過R關鍵字來引用任何一個對象,比如要引用上面的對象,可以使用2 0 R,需要主意的是,R關鍵字不僅可以引用一個已經定義的對象,還可以引用一個并不存在的對象,而且效果就和引用了一個空對象一樣。
(3)交叉引用表
交叉引用表是PDf文件內部一種特殊的文件組織方式,可以很方便的根據對象號隨機訪問一個對象。其格式如下:xref0 10000000000 65535 f4 10000000009 00000 n8 30000000074 00000 n0000000120 00000 n0000000179 00000 n - 其中,xref是開始標志,表示以下為一個交叉引用表的內容;
每個交叉引用表又可以分為若干個子段,每個子段的第一行是兩個數字,第一個是對象起始號,后面是連續的對象個數,接著每行是這個子段的每個對象的具體信息——每行的前10個數字代表這個這個對象相對文件頭的偏移地址,后面的5位數字是生成號(用于標記PDF的更新信息,和對象的生成號作用類似),最后一位f或n表示對象是否被使用(n表示使用,f表示被刪除或沒有用)。上面這個交叉引用表一共有3個子段,分別有1個,1個,3個對象,第一個子段的對象不可用,其余子段對象可用。
(4)文件尾 - 通過trailer可以快速的找到交叉引用表的位置,進而可以精確定位每一個對象;還可以通過它本身的字典還可以獲取文件的一些全局信息(作者,關鍵字,標題等),加密信息,等等。具體形式如下:trailer<<key1 value1key2 value2key3 value3…>>startxref553%%EOF
- trailer后面緊跟一個字典,包含若干鍵-值對。具體含義如下:
- 上面代碼中的startxref:后面的數字表示最后一個交叉引用表相對于文件起始位置的偏移量
%%EOF:文件結束符
2、邏輯文件結構
-
catalog根節點
catalog是整個PDF邏輯結構的根節點,這個可以通過trailer的Root字段定位,雖然簡單,但是相當重要,因為這里是PDF文件物理結構和邏輯結構的連接點。Catalog字典包含的信息非常多,這里僅就最主要的幾個字段做個說明。
字段類型值Typename(必須)只能為Pages 。
Parentdictionary(如果不是catalog里面指定的跟節點,則必須有,并且必須是間接對象) 當前節點的直接父節點。
Kidsarray(必須)一個間接對象組成的數組,節點可能是page或page tree。
Countinteger(必須) page tree里面所包含葉子節點(page 對象)的個數。
從以上字段可以看出,Pages最主要的功能就是組織所有的page對象。Page對象描述了一個PDF頁面的屬性、資源等信息。Page對象是一個字典,它主要包含一下幾個重要的屬性:
Pages字段 這是個必須字段,是PDF里面所有頁面的描述集合。Pages字段本身是個字典,它里面又包含了一下幾個主要字段:字段類型值 Type name (必須)必須是Page。 Parent dictionary (必須;并且只能是間接對象)當前page節點的直接父節點page tree 。 LastModified date (如果存在PieceInfo字段,就必須有,否則可選)記錄當前頁面被最后一次修改的日期和時間。 Resources dictionary (必須; 可繼承)記錄了當前page用到的所有資源。如果當前頁不用任何資源,則這是個空字典。忽略所有字段則表示繼承父節點的資源。 MediaBox rectangle (必須; 可繼承)定義了要顯示或打印頁面的物理媒介的區域(default user space units) CropBox rectangle (可選; 可繼承)定義了一個可視區域,當前頁被顯示或打印的時候,它的內容會被這個區域裁剪。默認值就是 MediaBox。 BleedBox rectangle (可選) 定義了一個區域,當輸出設備是個生產環境( production environment)的時候,頁面顯示的內容會被裁剪。默認值是 CropBox. Contents stream or array (可選) 描述頁面內容的流。如果這個字段缺省,則頁面上什么也不會顯示。這個值可以是一個流,也可以是由幾個流組成的一個數組。如果是數組,實際效果相當于所有的流是按順序連在一起的一個流,這就允許PDF生成的時候可以隨時插入圖片或其他資源。流之間的分割只是詞匯上的一個分割,并不是邏輯上或者組織形式的切割。 Rotate integer (可選; 可繼承) 順時鐘旋轉的角度數,這個必須是90的整數倍,默認是0。 Thumb stream (可選)定義當前頁的縮略圖。 Annots array (可選) 和當前頁面關聯的注釋。 Metadata stream (可選) 當前頁包含的元數據。 -
一個簡單例子:
3 0 obj<< /Type /Page/Parent 4 0 R/MediaBox [ 0 0 612 792 ]/Resources <</Font<</F3 7 0 R /F5 9 0 R /F7 11 0 R>>/ProcSet [ /PDF ]>>/Contents 12 0 R/Thumb 14 0 R/Annots [ 23 0 R 24 0 R]>> endobj -
Outlines字段
Outline是PDF里面為了方便用戶從PDF的一部分跳轉到另外一部分而設計的,有時候也叫書簽(Bookmark),它是一個樹狀結構,可以直觀的把PDF文件結構展現給用戶。用戶可以通過鼠標點擊來打開或者關閉某個outline項來實現交互,當打開一個outline時,用戶可以看到它的所有子節點,關閉一個outline的時候,這個outline的所有子節點會自動隱藏。并且,在點擊的時候,閱讀器會自動跳轉到outline對應的頁面位置。
Outlines包含以下幾個字段:
字段類型值Typename(可選)如果這個字段有值,則必須是Outlines。Firstdictionary(必須;必須是間接對象) 第一個頂層Outline item。Lastdictionary(必須;必須是間接對象)最后一個頂層outline item。Countinteger(必須)outline的所有層次的item的總數。 -
Outline是一個管理outline item的頂層對象,我們看到的,其實是outline item,這個里面才包含了文字、行為、目標區域等等。
一個outline item主要有一下幾個字段:
字段類型值Titletext string(必須)當前item要顯示的標題。
Parentdictionary(必須;必須是間接對象) outline層級中,當前item的父對象。如果item本身是頂級item,則父對象就是它本身。
Prevdictionary(除了每層的第一個item外,其他item必須有這個字段;必須是間接對象)當前層級中,此item的前一個item。
Nextdictionary(除了每層的最后一個item外,其他item必須有這個字段;必須是間接對象)當前層級中,此item的后一個item。
Firstdictionary(如果當前item有任何子節點,則這個字段是必須的;必須是間接對象) 當前item的第一個直接子節點。
Lastdictionary(如果當前item有任何子節點,則這個字段是必須的;必須是間接對象) 當前item的最后一個直接子節點。
Destname,byte string, or array(可選; 如果A字段存在,則這個不能被會略)當前的outline item被激活的時候,要顯示的區域。
Adictionary(可選; 如果Dest 字段存在,則這個不能被忽略)當前的outline item被激活的時候,要執行的動作。 -
URI字段 URI(uniform resource identifier),定義了文檔級別的統一資源標識符和相關鏈接信息。目錄和文檔中的鏈接就是通過這個字段來處理的.
-
Metadata字段 文檔的一些附帶信息,用xml表示,符合adobe的xmp規范。這個可以方便程序不用解析整個文件就能獲得文件的大致信息。
-
其他 Catalog字典中,常用的字段一般有以下一些:
字段類型值 Type name (必須)必須為Catalog。 Version name (可選)PDF文件所遵循的版本號(如果比文件頭指定的版本號高的話)。如果這個字段缺省或者文件頭指定的版本比這里的高,那就以文件頭為準。一個PDF生成程序可以通過更新這個字段的值來修改PDF文件版本號。 Pages dictionary (必須并且必須為間接對象)當前文檔的頁面集合入口。 PageLabels number tree (可選) number tree,定義了頁面和頁面label對應關系。 Names dictionary (可選)文檔的name字典。 Dests dictionary (可選;必須是間接對象)name和相應目標對應關系字典。 ViewerPreferences dictionary (可選)閱讀參數配置字典,定義了文檔被打開時候的行為。如果缺省,則使用閱讀器自己的配置。 PageLayout name (可選) 指定文檔被打開的時候頁面的布局方式。SinglePageDisplay 單頁OneColumnDisplay 單列TwoColumnLeftDisplay 雙列,奇數頁在左TwoColumnRightDisplay 雙列,奇數頁在右TwoPageLeft 雙頁,奇數頁在左TwoPageRight 雙頁,奇數頁在右缺省值: SinglePage. PageMode name (可選) 當文檔被打開時,指定文檔怎么顯示UseNone 目錄和縮略圖都不顯示UseOutlines 顯示目錄UseThumbs 顯示縮略圖FullScreen 全屏模式,沒有菜單,任何其他窗口UseOC 顯示Optional content group 面板UseAttachments顯示附件面板缺省值: UseNone. Outlines dictionary (可選;必須為間接對象)文檔的目錄字典 Threads array (可選;必須為間接對象)文章線索字典組成的數組。 OpenAction array or dictionary (可選) 指定一個區域或一個action,在文檔打開的時候顯示(區域)或者執行(action)。如果缺省,則會用默認縮放率顯示第一頁的頂部。 AA dictionary (可選)一個附加的動作字典,在全局范圍內定義了響應各種事件的action。 URI dictionary (可選)一個URI字典包含了文檔級別的URI action信息。 AcroForm dictionary (可選)文檔的交互式form (AcroForm)字典。 Metadata stream (可選;必須是間接對象)文檔包含的元數據流。
3、對象
- 對象主要有下面幾種
- booleam 用關鍵字true或false表示,可以是array對象的一個元素,或dictionary對象的一個條目。也可以用在PostScript計算函數里面,做為if或if esle的一個條件。
- numeric 包括整形和實型,不支持非十進制數字,不支持指數形式的數字。例: 1)整數 123 4567 +111 -2 范圍:正2的31次方-1到負的2的31次方 2)實數 12.3 0.8 +6.3 -4.01 -3. +.03 范圍:±3.403 ×10的38次方 ±1.175 × 10的-38次方
注意:如果整數超過表示范圍將轉化成實數,如果實數超過范圍就會出錯 - string 由一系列0-255之間的字節組成,一個string總長度不能超過65535.string有以下兩種方式:
十六進制字串 由<>包含起來的一個16進制串,兩位表示一個字符,不足兩位用0補齊。例:
表示AA和BB兩個字符 \ 表示AA和B0兩個字符
直接字串 由()包含起來的一個字串,中間可以使用轉義符"/"。例:(abc) 表示abc
(a//) 表示a/ - name 由一個前導/和后面一系列字符組成,最大長度為127。和string不同的是,name是不可分割的并且是唯一的,不可分割就是說一個name對象就是一個原子,比如/name,不能說n就是這個name的一個元素;唯一就是指兩個相同的name一定代表同一個對象。從pdf1.2開始,除了ascii的0,別的都可以用一個#加兩個十六進制的數字表示。例:
/name 表示name
/name#20is 表示name is
/name#200 表示name 0 - array 用[]包含的一組對象,可以是任何pdf對象(包括array)。雖然pdf只支持一維array,但可以通過array的嵌套實現任意維數的array(但是一個array的元素不能超過8191)。例:[549 3.14 false (Ralph) /SomeName]
- Dictionary 用"<<“和”>>"包含的若干組條目,每組條目都由key和value組成,其中key必須是name對象,并且一個dictionary內的key是唯一的;value可以是任何pdf的合法對象(包括dictionary對象)。例: << /IntegerItem 12 /StringItem (a string) /Subdictionary << /Item1 0.4 /Item2 true /LastItem (not!) /VeryLastItem (OK) >> >>
- stream 由一個字典和緊跟其后面的一組關鍵字stream和endstream以及這組關鍵字中間包含一系列字節組成。內容和string很相似,但有區別:stream可以分幾次讀取,分開使用不同的部分,string必須作為一個整體一次全部讀取使用;string有長度限制,但stream卻沒有這個限制。一般較大的數據都用stream表示。需要注意的是,stream必須是間接對象,并且stream的字典必須是直接對象。從1.2規范以后,stream可以以外部文件形式存在,這種情況下,解析PDF的時候stream和endstream之間的內容就被忽略掉。例: dictionary stream…data…endstreamstream字典中常用的字段如下:
字段名類型值Length整形(必須)關鍵字stream和endstream之間的數據長度,endstream之前可能會有一個多余的EOL標記,這個不計算在數據的長度中。Filter名字 或 數組(可選)Stream的編碼算法名稱(列表)。如果有多個,則數組中的編碼算法列表順序就是數據被編碼的順序。DecodeParms字典 或 數組(可選)一個參數字典或由參數字典組成的一個數組,供Filter使用。如果僅有一個Filter并且這個Filter需要參數,除非這個Filter的所有參數都已經給了默認值,否則的話 DecodeParms必須設置給Filter。如果有多個Filter,并且任意一個Filter使用了非默認的參數, DecodeParms 必須是個數組,每個元素對應一個Filter的參數列表(如果某個Filter無需參數或所有參數都有了默認值,就用空對象代替)。如果沒有Filter需要參數,或者所有Filter的參數都有默認值,DecodeParms 就被忽略了。F文件標識(可選)保存stream數據的文件。如果有這個字段, stream和endstream就被忽略,FFilter將會代替Filter, FDecodeParms將代替DecodeParms。Length字段還是表示stream和endstream之間數據的長度,但是通常此刻已經沒有數據了,長度是0.FFilter名字 或 字典(可選)和filter類似,針對外部文件。FDecodeParms字典 或 數組(可選)和DecodeParams類似,針對外部文件。 - Stream的編碼算法名稱(列表)。如果有多個,則數組中的編碼算法列表順序就是數據被編碼的順序。且需要被編碼。編碼算法主要如下:編碼可視化主要顯示為亂碼,所以提供了隱藏信息的機會,如下圖的steam內容為亂碼。
- NULL 用null表示,代表空。如果一個key的值為null,則這個key可以被忽略;如果引用一個不存在的object則等價于引用一個空對象。
3、PDF探索
概述總結:
- PDF文件本身是以文本/二進制混合內容組織的,有類似JSON的整數/浮點數、字符串、數組(列表)、字典等數據類型(見文檔3.2節)。大部分PDF強行扔到文本編輯器里根本看不出個所以然來,因為為了節省體積會對相當一部分內容進行壓縮。
- 有一個命令行工具mutool可以將壓縮的部分解壓,將整個PDF文件的內容展開成方便閱讀的純文本(同時仍能作為PDF正常打開,所有二進制內容以十六進制數字的編碼表示)。
- PDF的結構單元是一個又一個的數據塊,或者用PDF的術語就叫對象(object)。對象封裝了上述各種數據類型,并且對象之間可以相互引用。一些對象本身描述頁面的內容,另一些對象包含字體、圖片、書簽、注釋等信息,被描述頁面的對象所引用。有一個根對象負責將所有頁面和資源組織起來,如同數據結構里的樹。
- PDF會在文件末尾記錄各種對象在文件中的位置,并且指出根對象是哪一個。從根對象出發逐步解析各個對象,就能將每個頁面和它們需要的資源收集起來,最終將頁面渲染出來用于顯示或者打印。
MuPDF Command Line Tools
官方網站:https://mupdf.com/index.html
參考資料:
PDF解析:https://zxyle.github.io/PDF-Explained/
PDF操作:https://blog.csdn.net/qq_34286065/article/details/80295999
PDF一些py庫
https://github.com/pmaupin/pdfrw
https://github.com/pymupdf/PyMuPDF
總結
- 上一篇: URP管线理解(一)宏观入口
- 下一篇: 我是这样分析Linux性能问题的