轉載:pdf格式中文參考資料加個人註解
工作上需要修改apache 的 pdfbox, 所以參考關於pdf的格式說明
以下是我所能找到關於pdf 中文參考最詳盡的資料
原文出自
http://www.programmer-club.com.tw/ShowSameTitleN/general/3856.html
作者 chiuinan2(青衫)
有藍色的部分是我自己加的一些註解及實作心得, 紅色部分為我認為是
重點的地方純粹是自己學習pdf使用, 除了註解外, 所有著作權為
http://www.programmer-club.com.tw/ShowSameTitleN/general/3856.html
作者 chiuinan2(青衫) 所有
[心得] PDF檔案格式 (僅列抽取文字相關部份)
PDF檔是我解過所有的檔案裡最複雜的一個, 裡面牽涉的技術頗多, 因此如果不是非常必要, 我會建議各位採用別人的元件比較方便些.
-------------------------
1. PDF的檔案結構
標準的PDF檔係由下列四個區塊依序所組成:
a. header:內含版本資訊。
b. body:內含實際的文件內容。
c. cross-reference table(簡稱xref table):內含物件參照的相關資訊。
d. trailer:內含指向xref table、body區的重要相關資訊。
各區塊的實際內容,我們後面會再提到,現在先說明一下各區塊內的資料呈現方式。PDF是以行的方式來呈現資料的,每一行的結束字元,可以是Carriage Return(ASCII 13)、也可以是Line Feed(ASCII 10)、或是兩者的組合(ASCII 13緊接著ASCII 10)。
各行內的資料,若遇到%字元,表示該行從%字元開始後面的所有資料都是註解,必須加以略除。但有一個例外,那就是串流物件(stream object)的內容,並非以行的方式呈現,必須另行處理,這點我們後面提到資料物件時,會再加以說明。另外要注意的是,PDF裡的資料是大小寫有關的。
除了標準的PDF檔外,PDF檔是可以加以修改的,而修改的資料,便附加在原PDF檔資料的後面,因此其結構便成為:
header
{body, xref table, trailer}*
各修改的部份,便是利用trailer裡的指標來相互連結,這點在提到trailer內容時再來說明。除了附加修改的PDF檔外,還有一種特殊的PDF檔,稱為線性化PDF檔(Linearized PDF),這是為了適用於網頁傳遞快速顯示所使用的特殊結構,其特點如下:
(1) 整個檔案結構視為修改一次的結果(即有兩個body, xref table, trailer)
(2) 第一個body裡的第一行是註解,裡面包含>=128字碼的字元,body裡其他的資料則是線性化相關的參數
(3) 第二個body才是真正的PDF文件內容
(4) trailer和xref table的連結和正常的PDF檔不太一樣(在說明trailer區塊時再一併說明)
由於線性化PDF檔是以附加修改的PDF檔結構為基準,因此視為後者來處理仍然是正確的,這邊便不再加以區分是否為線性化PDF(只有讀取trailer區塊時要調整一下)。
2. PDF的檔頭
header區塊的資料,其實只有一行註解文字,且固定以”%PDF-“開頭,後面緊接著PDF的版本,例如:
%PDF-1.3
因此可藉由此字串來檢查是否為PDF檔。
3. 開始讀取的地方:trailer區塊
要讀取PDF檔的內容,必須由檔尾的trailer區塊開始讀取,(pdfbox是由trailer開始讀取)經由其內的指標來取得xref table與body內容。trailer區塊的內容如下:
trailer
<<
trailer資料
>>
startxref
xref table開始的檔案位置
%%EOF
trailer資料主要由{/屬性名稱 屬性值}*所組成,以下便是一個例子:
/Size 22
/Root 2 0 R
/Info 1 0 R
/Prev 408
以下則是各屬性的意義(這邊有許多物件結構,暫時不加以說明,大家先以邏輯觀念弄懂整個檔案結構後,再來說明會比較清楚些):
(1) Size:後面接的整數表示整個檔案的xref table的項目數
(2) Prev:如果有的話,後面的整數表示前次內容裡的xref table檔案位置
(3) Root:後面的資料表示文件裡Catalog的物件編號(等要讀取body區塊時再說明)
(4) Info:如果有的話,後面的資料表示文件裡的摘要資訊所在物件編號(等要讀取body區塊時再說明)
(5) ID:如果有的話,後面的陣列資料分別表示舊ID與新ID。這個資料主要做為外部PDF檔連結用,對我們而言並無意義,但其中的第一個ID與加密有關,需要注意
(6) Encrypt:如果有的話,表示PDF檔有加密,其後接的詞典資料,便是用來解密用的(這部份等最後面再來說明)
(如果加密的pdf, 沒有加入Encrypt的模組, 則取出的stream會是錯的)
Bouncy Castle
Java 密碼擴展(JCE)的提供者。
Spongy Castle
Spongy Castle 對最新版本的 BouncyCastle 進行了簡單地重新打包;適用android系統, 所有的 org.bouncycastle.* 包重命名為了 org.spongycastle.*,所有 Java安全 API 提供者的名字由 BC 改為了 SC。
原則上,PDF檔應該要讀取檔尾的最後一個trailer區塊為準,而且只需讀取一個(即使有多個trailer區塊)。但對於線性化PDF檔而言,其應讀取的trailer區塊則是第一個,同時首頁所需的物件參照資訊放在第一個xref table,其餘各頁的物件參照資訊則放到第二個xref table(這樣做的目的,當然是要讓PDF檔尚未傳完時,便能讀取所需的相關資訊,然後當收到body區塊資料時,便能立即顯示第一頁出來)。為了適用於線性化PDF檔,讀取trailer區塊的方式,必須改成:
(1) 由檔尾trailer區塊裡找到startxref,取得xref table開始的檔案位置
(2) 移到該xref table的位置,略過xref table,讀取後面緊接著的trailer區塊內容
由於正常的PDF檔,檔尾trailer區塊startxref指向的xref table都是最後一個xref table,因此略過該xref table資料後,還是會找到檔尾的trailer區塊,並沒有什麼不同。這樣的作法,只是為了同時適用於線性化PDF檔而已。
除了前述整體的文件相關資訊外,trailer區塊另一重要的作用,便是經由其內的指標,讀取所有的xref table資料,為了也同時適用於線性化PDF檔,整個讀取trailer和xref table的過程應為:
(1) 由檔尾trailer區塊裡找到startxref,取得xref table開始的檔案位置
(2) 移到該xref table的位置,略過xref table,讀取後面緊接著的trailer區塊內容
(3) 移到該xref table的位置,開始讀取xref table內容
(4) 找尋其後緊接的trailer區塊中是否有Prev屬性,沒有即結束
(5) 如果有Prev,則其後的數字視為下個xref table的檔案位置,回到步驟3
4. xref table區塊
xref table區塊的格式如下:
xref
物件參照表*
每個物件參照表的格式如下:
物件開始編號 參照表項目數
參照表項目*
每個參照表項目固定20 byte,其格式如下:
(1) byte 0-9:物件資料所在的檔案位置,靠右,不足時補0
(2) byte 10:空白字元
(3) byte 11-15:代數(generation number),靠右,不足時補0
(4) byte 16:空白字元
(5) byte 17:n表示物件使用中,f表示物件未被使用(free)
(6) byte 18-19:空白與換行字元
以下便是一個xref table的例子:
xref
0 3
000000003 65535 f
000000127 00000 n
000000000 00001 f
5 1
000004346 00000 n
這個例子表示物件編號0、2、3、4未被使用,物件編號1、5的資料在127、4346的位置裡。其實xref table區塊的資料,就是用來記錄文件內容裡的各物件資料所在的位置。由於PDF檔是可被修改的,因此文件內容的物件一但有被修改的狀況,xref table裡便會記錄下來,以便參照到該物件時,會去讀取新的物件資料,而不是舊的物件資料。比較要注意的是被刪除的狀況,當物件被刪除時,xref table裡會記錄該物件是未被使用的,同時代數會加一,若之後物件有增加時,便可以使用該物件編號。代數改變的目的,是要有所標記,以避免參用到被刪除前的物件資料(因為當參用物件時,除了物件編號,還必須附上代數)。代數不能>=65535,一但達到這個數字,該物件編號便不能再使用,而必須另行增加一個編號。由於我們只是要讀取文件內容資料而已,這部份其實不必管那麼多,只需對所有xref table區塊裡的內容,由新到舊一個一個讀入即可(同一物件編號需以新的為準,舊的不能蓋掉新的),最後形成一個以物件編號做為索引的物件參照表陣列,供文件內容使用。
這邊必須要注意的是,如果xref table或trailer資料不正確時(包括指標值),有可能相關資料毀損,此時必須嘗試掃描整個檔案,重建出xref table與trailer資料才行(trailer必須有Root屬性)。
5. 物件的形態與參用
為了方便後續的說明,因此這裡先介紹一下PDF檔裡的各種物件形態。首先說明物件的參用形式如下:
物件編號 代數 R
大家可以看一下前述trailer區塊裡的Root與Info屬性值做為例子。在文件內容,甚至是物件內容裡,都可能有物件參用形式。例如一個影像的串流資料物件,當PDF writer在寫入時,並不知道它的長度(指one pass產生而言),因此在長度欄裡便可能參用一個整數物件,等寫完後得知長度時,再加入該參用的整數物件,裡面內含該長度的整數數值。為了方便後續的說明,以下我們便先說明各種物件的形態與資料表示方式。一個物件的基本形式如下:
物件編號 代數 obj
物件資料
endobj
物件的形態便是由物件資料來決定的,每個物件只能有一種物件資料。以下便是各種物件的資料表示方式(包括前述的物件參用形式都算):
(1) 布林值物件
只有兩種資料:true和false,利用這兩個字做為區別
(2) 數字物件
包括整數和實數(含負數),實數必須以小數點的形式出現。因此開頭是數字或負號者,便是數字物件。不過由於物件參用形式是整數+整數+R,因此整數物件尚必須往後看兩個物件,才能得知是否為數字物件,還是物件參用形式。
(3) 字串物件
字串物件有兩種表示法,一般可見字元用括號(),16進制表示法用角號<>表示,例如:
(This is a string)
<4e6f762073686d6f7a206b6120706f702e>4e6f762073686d6f7a206b6120706f702e>
注意可見字元包括CR與LF等。如果一行太長寫不下去,則可以用反斜線(backslash,\)做為續行動作,例如:
(These \
two strings \
are the same.)
等於下面這行:
(These two strings are the same.)
另外,也可使用反斜線做為跳逸字元,跳逸字元如下表所示,若出現沒有在下表中的跳逸字元,將會被忽略不顯示。
跳逸字元 說明
\n 換行(Line feed)
\r 歸位(Carriage return)
\t 定位(Horizontal tab)
\b 倒退(Backspace)
\f 跳頁(Form feed)
\( 左括號(Left parenthesis)
\) 右括號(Right parenthesis)
\\ 反斜線(Backslash)
\ddd 8進位轉換字元(Character code ddd (octal))
其中8進位轉換字元並不一定要3個數字字元,只要遇到非0-7的數字,便視為數字結束(但一般建議是補足3個)。必須注意的是,8進位轉換出來的字元是允許ASCII 0的NULL字元的。另外,在()裡的%並不視為註解起始字元,而是視為字串資料的一部份,同時在()裡若還有成對的(),可不必使用跳逸字元,在處理時要特別注意此一情況。
至於16進制的資料必須是成對的,若出現不成對的情況,則必須自動補0,例如<901fa>,需算成有3個byte的字串,分別為90、1F及A0。字串裡的空白字元需略過。901fa>
當PDF檔有加密時(可由trailer區塊得知),字串物件裡的字串都是加密的,必須解密才行,這點以後再說明。
(4) 名稱物件
名稱物件的起始字元為/,其後緊接著名稱字串(大小寫有關),例如/Title(trailer區塊裡的屬性便是名稱物件)。名稱字串裡的字元必須在ASCII 33~255之間,且不能是%()<>[]{}/#這幾個字元。如果名稱中希望含有其它不合法的字元時(ASCII 0的NULL字元除外),必須採用#加兩個16進位數字的方式來表示字元碼。例如想加入一個空白字元,則可用/Adobe#20Green,實際名稱便是Adobe Green。
(5) 陣列物件
陣列物件的格式如下:
[物件*]
在[]裡的物件資料,可以是任何一種物件,包括物件參用形式,例如:
[0 (string) /Name 1 0 R]
這個陣列裡總共有4個物件資料。
(6) 詞典物件
詞典物件的格式如下:
<<
詞典項目*
>>
各詞典項目的格式如下:
名稱物件 物件資料
前面便是屬性名稱,後面便是它的屬性值,可以是任何一種物件,包括物件參用形式。例如trailer區塊,本身便是一個詞典物件。
(7) 串流物件
串流物件主要記錄一連續的任何資料(例如影像資料),其格式如下:
詞典物件
stream
串流資料
endstream
其中stream之後必須以CR+LF或LF結束,不能單純以CR做結束,在LF之後才是真正的串流資料。
在stream前的詞典物件,主要用來說明該串流資料的特性,以下為各屬性的意義:
a. Length:後面的整數表示串流資料裡的字元數(介於stream和endstream之間)
b. Filter:如果有的話,後面的名稱或名稱陣列,表示串流資料應使用的解壓方法與次序(後述)
c. DecodeParms:如果有的話,後面的資料即前述解壓方法所需的參數
d. F:如果有的話,後面的資料表示串流資料所在的檔案名稱,此時該串流資料應被略過不理
e. FFilter:同Filter,但作用於外部檔案的串流資料
f. FDecodeParams:同DecodeParms,但作用於外部檔案的串流資料
以下是Filter裡可能的解壓法名稱:
(在pdfbox中的實作部分在 class FilterFactory)
名稱 有無參數 說明
ASCIIHexDecode或AHx 無 使用16進制ASCII編碼,適用在二元碼資料
ASCII85Decode或A85 無 使用ASCII base-85編碼,適用在二元碼資料
LZWDecode或LZW 有 使用LZW方式壓縮,適用在二元碼及純文字資料
FlateDecode或Fl 有 使用zlib/deflate方式壓縮,適用在二元碼及純文字資料
RunLengthDecode或RL 無 使用byte-oriented run-length演算法編碼,適用在二元碼及純文字資料
CCITTFaxDecode或CCF 有 使用CCITT編碼,適用在單色圖形資料
DCTDecode或DCT 有 使用DCT編碼,適用在一般圖形資料
關於各解壓法的演算法,我們留到後面再來說明。
(8) 空物件
以null來表示。
說明完各種物件後,下面總結一下各種物件的辨識方法:
(1) 以(開頭:字串物件
(2) 以/開頭:名稱物件
(3) 以<開頭:若後面不接<,便是字串物件(見<<開頭的說明)
(4) 以<<開頭:詞典物件,若之後再接stream便是串流物件
(5) 以負號開頭:後面接數字便是數字物件
(6) 以數字開頭:數字物件,整數物件必須再往後看兩個物件,才能決定是否為物件參用形式
(7) 以f開頭:若是false便是布林物件
(8) 以n開頭:若是null便是空物件
(9) 以t開頭:若是true便是布林物件
(10) 以〔開頭:陣列物件
(11) 其他:不合法的物件
6. stream資料的解壓方法
(1) ASCIIHexDecode
ASCIIHex的編碼方式即是將輸入字元編成2個16進位碼,然後以>做為結束,因此在解壓時,只需每次讀取兩個字元進行解碼還原即可,但在取字元時,要注意需略過空白字元(含換行字元等)。
(2) ASCII85Decode
ASCII85是將輸入資料每次取4個byte,然後轉換成5個byte輸出。假設輸入資料為b1~b4,輸出資料為c1~c5,則其關係為:
b1*256^3 + b2*256^2 + b3*256 + b4 = c1*85^4 + c2*85^3 + c3*85^2 + c4*85 + c5
也就是將256基底改成85基底。但是在輸出時,必須將資料值加上33,使得輸出字元碼在33-117之間。但有一些例外:
(1) 如果5個輸出資料值都是0,則改以一個z字元(ASCII 122)取代
(2) 如果輸入資料不足4 byte時(假設n個),則後面補0計算後,取前n+1個資料值輸出
資料結束係以~>連續兩個字元表示。解壓時同樣要略過空白字元。
(3) LZWDecode
使用標準LZW解壓。它有幾個參數屬性值如下:
a. Predict:如果有的話,後接的整數值>1,表示需要做影像相關的壓縮調整
b. EarlyChange:如果有的話,後接的整數值若為0,表示編碼表需儘量延後擴增(如果是1則會提前一個字碼擴增,內定為1)
LZW的解壓方式很簡單,但在說明解壓步驟之前,需要先說明一下LZW字串表的結構。每個字串表項目主要分成下列三個部份:
Prefix Suffix 字串
字串當然就是字串表記錄的字串(供解壓輸出用),而該字串主要由Prefix和Suffix所結合而成。Suffix一定是一個字元,Prefix可以是一個字元,也可以指向字串表裡的一個項目。由於字串可由Prefix和Suffix所組成,因此也可不必記錄字串,在需要時,再利用項目裡的Prefix和Suffix值組合出來(但速度會較慢)。因為LZW的項目表大小規定最多是4096個,因此Prefix、Suffix這兩個數值使用32 bit的長整數便可以表示。至於字串表的初始資料,由於壓縮資料的字碼範圍為0~255,因此字串表一開始的256個項目便保留做為記錄該字元使用。除此之外,還必須再加兩個特殊字碼:256表示必須重置字串表;257表示資料結束。以下便是LZW的解壓步驟:
a. 重置字串表,將之初始化為258個項目,將CodeLength設為9
b. 由輸入資料中取出CodeLength個位元組成輸入碼(資料不足便有問題)
c. 若之前尚未處理過任何輸入資料(即重置字串表後第一次處理輸入資料),則將Prefix設成輸入碼,並檢查輸入碼,若是256便回到步驟a,若是257便結束,都不是則回到步驟b
d. 將Suffix設成輸入碼,並檢驗Suffix是否為一個字元(<258 br="" refix="" uffix="">e. 將Prefix輸出(注意Prefix可能是一個字元,也可能指向字串表裡的一個項目)
f. 檢查輸入碼,若是256便回到步驟a,若是257便結束
g. 若字串表項數<4096 br="" refix="" uffix="">h. 比對字串表的大小與CodeLength可容納的空間(9=512,10=1024,11=2048,12=4096),如果相等,則將CodeLength加一(注意若是EarlyChange,則CodeLength可容納空間要減一,亦即字串表還剩一個時便擴增字串表),若CodeLength>=12便不再增加(即CodeLength最大只能12)
i. 將Prefix設成輸入碼,回到步驟b4096>258>
<258 br="" refix="" uffix=""><4096 br="" refix="" uffix="">
4096>258>
(4) FlateDecode
由於資料是使用zlib裡的deflate函數壓縮,因此只需用zlib裡的inflate函數解壓即可,和PowerPoint裡的Embeded Object解壓方式相同。但要注意Predict屬性值不能>1(同LZWDecode)。(zip的壓縮法, 最常用)
(5) RunLengthDecode
RunLength壓縮法,是將輸入資料做成下列形式:
長度byte 資料
(1) 長度byte值在0~127時,表示後面接 (長度byte值+1) 個未編碼資料
(2) 長度byte值在129~255時,表示後面接一個資料,必須重複 (257-長度byte值) 次
(3) 長度byte值為128時,表示資料結束
(6) CCITTFaxDecode
這個壓縮法主要用在影像資料上,我們用不著。
(7) DCTDecode
這個壓縮法主要用在影像資料上,我們用不著。
7. 摘要資訊
PDF檔的摘要資訊是放在由trailer區塊裡Info屬性指向的物件,該物件是一個詞典物件,我們需要的只有Author、Title、Subject這幾個屬性值(都是字串資料),其他屬性對我們而言都無意義。不過就我測試結果,PDF檔裡的摘要資訊大多沒有意義,因此也可不抓取。
8. Catalog與Page Tree
由trailer區塊裡Root屬性指向的物件,便是文件內容最上層的物件。該物件為一詞典物件,稱為Catalog物件。以下便是這個詞典物件裡重要的屬性意義如下(對於取出文字內容無用的都不列):
(1) Type:後接名稱物件,必須是Catalog
(2) Pages:指向Page Tree的根節點
所有PDF檔裡各頁的內容,都必須透過Page Tree去取得。Page Tree裡有兩種物件(都是辭典物件),一種是Pages物件,一種是Page物件。Page物件記錄了一頁實際的內容,Pages物件則是用來將Page物件組成Page Tree。以下便是Pages物件的基本屬性意義:
(1) Type:後接名稱物件,必須是Pages
(2) Kids:後接陣列物件,指向所有下層的節點(可能是Pages物件,也可能是Page物件)
(3) Count:後接數字物件,說明本節點開始的子樹共有多少Page物件(即有多少頁)
(4) Parent:指向上層的節點(根節點沒有)
下面則是Page物件的基本屬性意義:
(1) Type:後接名稱物件,必須是Page
(2) Parent:指向上層的節點
(3) Contents:指向文件的內容物件(必須是串流物件,或是由串流物件組成的陣列物件),如果沒有表示空頁
(4) Annots:後接陣列物件,說明本頁的文字註解、影片、聲音等等非顯示/列印用的相關資訊(目前暫不抓取)
除了上述的基本屬性外,下面還有一些是Pages物件與Page物件可共有的重要屬性,當省略時,一律繼承父節點(若一直到根節點都沒有,便用內定值):
(1) Resources:後接詞典物件,說明各頁顯示時所需的額外資源
Resources詞典裡重要的屬性為:
(1) Font:如果有的話,後接的詞典物件為各個字型名稱及其資訊
9. 頁描述命令
PDF檔內各頁的內容,都是記錄在Page物件裡Content屬性值指向的串流物件。然而是否解出這些串流物件資料,就能簡單地抽出文字呢?答案是否定的。要知道PDF檔的目的在於文件的可攜性,其重點在於原文重現,因此PDF各頁的資料,幾乎都是在描述版面上各種點、線、字、圖的顯現方式,並不管文字的順序與內碼語系,這和一般文件編輯程式的存檔有著極大的差異。因此想要抽出PDF檔各頁的文字資料,首先必須了解描述這些版面圖文的語言,也就是頁描述命令。但是在說明這些頁描述命令前,有些排版觀念需要先讓大家了解一下:
(1) PDF writer主要是接收排版軟體或其他類似軟體的列印輸出結果,因此在PDF裡每頁內容的記錄方式均取決於這些軟體的列印輸出方式。有些軟體可能輸出一串文字,有些則是一個字母一個字母輸出,有些則是遇到空白字元就不輸出,情況都不太一樣(像首字大寫時,便可能一個單字分兩次輸出)。因此我們無法從輸出的文字部份判斷是否為一個完整的單字,唯一的方法便是從文字串的列印位置與寬高範圍來加以判斷。
(2) 排版中有許多特殊效果,例如陰影,便是藉由字的重疊列印來產生的,在這種情況下,如果單純去取得文字,會變成一堆重複的字串,即使人眼看起來只有一個字串。要處理這種情況,仍然需要得到文字串的列印位置與寬高範圍,才能加以判斷。
(3) 通常列印的結果,都會有書眉、頁碼等附加在各頁的額外列印資料,有可能各頁都有,或是奇偶頁各自不同,更有可能首頁沒有、其餘各頁有,或是章節變動時又變更書眉與頁碼內容。再加上書眉、頁碼出現的位置,隨著設定的不同,可能出現在頁面上下左右任何一個位置,如果單純要從列印位置去切割這些資料出來,會是相當困難的一件事。然而我們可以從排版軟體列印輸出的習慣上來著手。由於內頁資料是一整體的,排版軟體幾乎都是一次連續輸出完畢,至於書眉、頁碼等附加資料,通常會在一頁最開始或最後面才附加列印上去(除非有其他註記資料)。因此我們可以直接取得每頁文字內容後,再依照上述排版業界對書眉、頁碼的列印習慣,去比對各頁最前面或最後面的字串,便能加以區分出來(至於版權頁便很難區分)。
(4) 要判斷連續的文字串資料是否同屬一個段落、連接時是否要加空白等等,首先要知道各行文字的排印方式。所有排版軟體對於文字的排印,必定是延著基準線往某個方向串接依序排印。如果將之想成座標軸的話,一般都是延著x軸正向列印,特別是列印英文資料的情況下,但也可能反向列印(例如中文便有由右到左與由左到右兩種列印習慣)。至於這個座標軸有可能旋轉任何角度,也就是直排/橫排、紙張直擺/橫擺的區分(也可能是要製作特殊效果的DM稿)。座標軸也可能變形,例如圓弧散狀的文字,便是將圓弧視為x軸,延著線排印文字。但無論如何變形,基本的排版計算方式都大同小異,這點對於要判斷文字接合上是很重要的。不過變形的座標軸,由字的座標是很難推算出來變形的狀況,在這種情況下,便很容易有誤判的情況產生,但目前也沒有什麼好方法可以解決,唯一的解法,大概只有用詞典的方式來判斷了。
綜合上述,我們目前的首要工作,便是取得各文字的列印位置與寬高範圍,因此後面的頁描述命令,我們將特別著重於這部份,其餘命令則可以略過不理。但在提到這些命令前,我們還要說明一下PDF檔裡使用的空間座標關係。PDF檔裡的空間座標一共有下列7種:
(1) 列印設備空間座標(Device Space):也就是實際要輸出的螢幕、印表機的空間座標。
(2) 使用者空間座標(User Space):為PDF檔內運算的基準空間座標,輸出時再轉為列印設備空間座標。
(3) 文字空間座標(Text Space):文字列印時運算的空間座標,必須再轉為使用者空間座標。
(4) 影像空間座標(Image Space):影像列印時運算的空間座標,必須再轉為使用者空間座標。
(5) XObject空間座標(Form Space):XObject資料列印時運算的空間座標,必須再轉為使用者空間座標(XObject是一個串流物件,內含Postscript命令)。
(6) 色彩空間座標(Pattern Space):定義Rendering、Shading等特殊效果時的空間座標,必須再轉為使用者空間座標。
(7) 字型空間座標(Character Space):字型內各文字屬性定義使用的空間座標,在列印文字時需轉為文字空間座標。
由於空間座標眾多,在頁描述命令裡便有許多的命令在定義轉換時使用的運算矩陣。如果我們要實際模擬出PDF各頁的顯示,當然需要去了解這些空間座標的定義與轉換,但如果只是要單純抽取出文字,事實上並不需要這麼麻煩,只需了解文字空間座標轉換成使用者空間座標即可。文字空間座標轉換矩陣由頁描述命令裡的Tm命令所設定,共有6個實數參數,假設為a b c d e f,則在文字空間座標上的一點(x,y),轉換後在使用者空間座標的位置為:
x’ = ax + cy + e
y’ = bx + dy + f
至於寬高則為:
width’ = a*width + c*height
height’ = b*width + d*height
現在我們開始說明PDF檔的頁描述命令,這些命令都是採用倒裝句的語法進行,也就是:
參數1 參數2 … 命令
當遇到命令時,再將前面收集到的參數送入執行。以下為各頁描述命令的意義與參數,需要處理的部份以粗體顯示,其餘部份不必了解太多(必須要有排版與繪圖學相關的知識才看得懂):
pdfbox中處理每個指令的定義在
PDFTextStreamEngine中的addOperator(new DrawObject());
參數數目:0
意義:封閉區域後塗網
(2) B:
參數數目:0
意義:對封閉區域塗網與塗線
(3) b*:
參數數目:0
意義:封閉區域後進行互斥塗網與塗線
(4) B*:
參數數目:0
意義:對封閉區域進行互斥塗網與塗線
(5) BDC:
參數數目:2
參數1:tag名稱(名稱物件)
參數2:屬性表(名稱物件或詞典物件)
意義:標記內容開始
(6) BI:
參數數目:0
意義:內插影像資料的開始,必須特別處理以略過影像資料
(7) BMC:
參數數目:1
參數:tag名稱(名稱物件)
意義:標記內容開始
(8) BT:
參數數目:0
意義:開始一個文字物件,並重設文字座標轉換矩陣與文字游標位置
(9) BX:
參數數目:0
意義:設定允許未定義的運算元
(10) c:
參數數目:6
參數:實數,分別表示3組(x,y)
意義:畫一條Bezier曲線
(11) cm:
參數數目:6
參數:實數
意義:設定CTM座標轉換矩陣
(12) cs:
參數數目:1
參數1:屬性名稱(名稱物件),可由額外資源中取得相關數值
意義:設定畫線時的色彩值
(13) CS:
參數數目:1
參數1:屬性名稱(名稱物件),可由額外資源中取得相關數值
意義:設定斷線時的色彩值
(14) d:
參數數目:2
參數1:整數陣列,內定空陣列(實線)
參數2:整數,內定0
意義:設定虛線形狀
(15) d0:
參數數目:2
參數:實數
意義:同postscript Type 3字型的setcharwidth,我們用不著
(16) d1:
參數數目:2
參數:實數
意義:同postscript Type 3字型的setcachedevice,我們用不著
(17) Do:
參數數目:1
參數:名稱物件,可由額外資源中取得XObject資料
意義:處理一個XObject資料
(18) DP:
參數數目:2
參數1:tag名稱(名稱物件)
參數2:屬性表(名稱物件或詞典物件)
意義:設定標記
(19) EI:
參數數目:0
意義:內插影像資料的結束,必須特別處理以略過影像資料
(20) EMC:
參數數目:0
意義:標記內容結束
(21) ET:
參數數目:0
意義:結束一個文字物件
(22) EX:
參數數目:0
意義:設定不允許未定義的運算元
(23) f:
參數數目:0
意義:對封閉區域塗網
(24) F:
參數數目:0
意義:對封閉區域塗網
(25) f*:
參數數目:0
意義:對封閉區域進行互斥塗網
(26) g:
參數數目:1
參數1:實數,0-1
意義:設定畫線時的灰度值
(27) G:
參數數目:1
參數1:實數,0-1
意義:設定斷線時的灰度值
(28) gs
參數數目:1
參數1:屬性名稱(名稱物件),可由額外資源中取得一個詞典物件
意義:額外的繪圖設定
(29) h:
參數數目:0
意義:封閉區域
(30) i:
參數數目:1
參數1:整數,0-100,內定為0
意義:設定曲線模擬時允許的誤差點數
(31) ID:
參數數目:0
意義:內插影像實際資料的開始,必須特別處理以略過影像資料
(32) j:
參數數目:1
參數1:整數,0-2,內定為0
意義:設定線段相接時的接點形狀
(33) J:
參數數目:1
參數1:整數,0-2,內定為0
意義:設定線段端點的形狀
(34) k:
參數數目:4
參數1-4:實數,0-1,分別表示CMYK
意義:設定畫線時的色彩值
(35) K:
參數數目:4
參數1-4:實數,0-1,分別表示CMYK
意義:設定斷線時的色彩值
(36) l:
參數數目:2
參數:實數,分別表示x和y
意義:畫一直線
(37) m:
參數數目:2
參數:實數,分別表示x和y
意義:將游標移到指定位置
(38) M:
參數數目:1
參數1:整數,>=1,內定為10
意義:設定線段相接時,若採用尖角形狀,其尖角最大允許的長度
(39) MP:
參數數目:1
參數:tag名稱(名稱物件)
意義:設定標記
(40) n:
參數數目:0
意義:結束封閉區域物件
(41) q:
參數數目:0
意義:儲存繪圖狀態
(42) Q:
參數數目:0
意義:回復繪圖狀態
回復繪圖狀態時, 也會回復字型資訊, 轉文字檔時, 此Q,q指令也要考慮
(43) re:
參數數目:4
參數1-2:實數,分別表示(x,y)
參數3-4:實數,分別表示寬和高
意義:畫出一個矩形
(44) rg:
參數數目:3
參數1-3:實數,0-1,分別表示RGB
意義:設定畫線時的色彩值
(45) RG:
參數數目:3
參數1-3:實數,0-1,分別表示RGB
意義:設定斷線時的色彩值
(46) ri:
參數數目:1
參數:名稱物件
意義:設定Rendering的方式
(47) s:
參數數目:0
意義:封閉區域後塗線
(48) S:
參數數目:0
意義:對連接線段塗色
(49) sc:
參數數目:4
參數1-4:實數,0-1
意義:設定畫線時的色彩值
(50) SC:
參數數目:4
參數1-4:實數,0-1
意義:設定斷線時的色彩值
(51) scn:
參數數目:不定
參數:實數,0-1,最後一個可以是名稱物件
意義:設定畫線時的色彩值
(52) SCN:
參數數目:不定
參數:實數,0-1,最後一個可以是名稱物件
意義:設定斷線時的色彩值
(53) sh:
參數數目:1
參數:名稱物件,可由額外資源裡的Shading屬性值(詞典物件)中取得相關參數
意義:對封閉區域繪置陰影
(54) Tc:
參數數目:1
參數:實數,內定為0
意義:設定字元間隔
(55) Td:
參數數目:2
參數:實數,表示(x,y)
意義:文字行游標偏移(x,y)位置
說明:
Reading Horizons
Volume 34, Issue 1 1993 Article 3
S EPTEMBER /O CTOBER 1993
Ribtickling Literature: Educational
Implications for Joke and Riddle Books in the
Elementary Classroom
上方為pdf文件範例, 用來說明Td指令
215 699 Td (剛開始時以左側為原點, x坐標距左邊界215, Y坐標距下邊界699開始印)
[(Reading)(20) (Horizons)] TJ
BT,q,Q 等指令之後, 坐標又回到原點
130 670 Td (以左側為原點, x坐標距左邊界130, Y坐標距下邊界670開始印)
[(V)(10)(olume)] TJ
38,0 Td
[(34,)] TJ (因沒有回復原點的指令, 所以以上次Td的開始坐標(第一個字元的位置)為參考點, X坐標為距V字母右方38,Y坐標不變開始印)
17,0 Td
[(Issue)] TJ (同上, X坐標為距數字3右方17,Y坐標不變開始印)
227,0 Td
[(Article)] TJ (同上, X坐標為距數字1右方227,Y坐標不變開始印)
35.53,0 Td
[(3)] TJ (同上, X坐標為距字母A右方35.53,Y坐標不變開始印)
-258.773,-19.825 Td
[(S)] TJ(Y坐標 -19.825, 表示距上次Y的位置下移19.825, 故已換行, X坐標為距上次位置退回 -258.773 開始印)
8.694,0 Td
[(EPTEMBER)] TJ
(56) TD:
參數數目:2
參數:實數,表示(x,y)
意義:文字行游標偏移(x,y),並將行距設成-y
(57) Tf:
參數數目:2
參數1:字形名稱,可由額外資源取得相關資訊
參數2:實數,字形大小
意義:設定字形與大小
(58) Tj:
參數數目:1
參數:字串物件
意義:顯示文字
(59) TJ:
參數數目:1
參數:陣列物件,內含字串物件與數字物件
意義:顯示文字,數字物件用來表示文字顯示後接的文字應顯示之游標位置,需延基準線回減多少距離,單位是字形大小/1000
數字物件的意義:
Positive numbers shift the next glyph to the left (decreasing glyph spacing to next glyph).
正數:表下一個字往左靠(住上個字接近)
Negative numbers shift the next glyph to the right (adding more space to next glyph).
負數:表下一個字往右靠(離上個字推遠)
The numbers themselves are to be taken as representing one thousandths of the current unit.
(60) TL:
參數數目:1
參數:實數,內定為0
意義:設定行距(基準線之間的距離),只作用於T*、’、”這三個命令
(61) Tm:
參數數目:6
參數:實數,前4個做為旋轉縮放用,後2個做為平移用
意義:設定文字座標轉換矩陣,並重設文字游標到原點
每次都會重設文字游標, 所以x,y坐標每次都是重原點計算
(62) Tr:
參數數目:1
參數:整數,內定為0
意義:設定文字的Rendering方式
(63) Ts:
參數數目:1
參數:實數,內定為0
意義:設定文字垂直偏移量
(64) Tw:
參數數目:1
參數:實數,內定為0
意義:設定單字間隔(空白字元額外加的間隔)
(65) Tz:
參數數目:1
參數:實數,內定為100
意義:設定文字水平寬度放大倍率
(66) T*:
參數數目:0
意義:將文字行游標移到下一行
(67) v:
參數數目:4
參數:實數,分別表示2組(x,y)
意義:畫一條Bezier曲線
(68) w:
參數數目:1
參數1:整數,內定為1
意義:設定線粗
(69) W:clip 339
參數數目:0
意義:切割繪製區域
(70) W*:
參數數目:0
意義:互斥切割繪製區域
(71) y:
參數數目:4
參數:實數,分別表示2組(x,y)
意義:畫一條Bezier曲線
(72) ‘:
參數數目:1
參數:字串物件
意義:將文字行游標移到下一行,並顯示文字
(73) “:
參數數目:3
參數1:實數,單字間隔
參數2:實數,字元間隔
參數3:字串物件
意義:設定單字間隔、字元間隔後,將文字行游標移到下一行,並顯示文字
注意上述的內插影像資料必須特別處理略過,其格式為:
BI
屬性與鍵值*
ID
影像資料
EI
另外,文字在顯示時,係以文字游標為準,但所有會移動游標的頁描述命令,均為移動文字行游標。也就是說,在顯示字串後,文字游標會依顯示字串寬度沿基準線移動,供下個字串顯示,但只要文字行游標一變動,文字游標便必須回到文字行游標的位置。
10. 字型
從頁描述命令中,我們可以知道文字列印的位置與高度(字型大小),但它的寬度則必須取決於字型。
除此之外,字型同時也決定了字碼,因此底下我們開始說明一下PDF檔裡的字型資訊。字型資訊是放在額外資源裡(Pages或Page物件的Resources屬性值),它本身是一個詞典物件。以下為其相關的屬性:
(1) Type:後接名稱物件,必須是Font
(2) Subtype:後接的名稱物件表示其字型型態,可能是Type0、Type1、MMType1、Type3、TrueType
(3) ToUnicode:如果有的話,後接的串流物件表示用來轉換成Unicode字碼的CMap(後述)
如果有ToUnicode的話表示這個字型只要靠PDF檔中embedded(內嵌)的轉換資料就可轉成UniCode而不必藉由讀取外部的轉換檔來轉換
其他一些屬性值,依字型型態不同而不同,以下便一一分別說明(注意字型空間座標的單位為文字空間座標/1000):
a. Type1
下面是Type1字型的相關屬性:
(1) BaseFont:後接的名稱物件為Postscript對應的字型名稱,若有+號表示為字型的子集合,格式為”標記+Postscript對應字型名稱”
(2) FirstChar:後接的整數表Widths陣列對應的第一個字元碼
(3) LastChar:後接的整數表Widths陣列對應的最後一個字元碼
(4) Widths:後接的整數陣列表示FirstChar到LastChar各字元的寬度,不在裡面的字元則必須使用FontDescriptor裡的MissingWidth資料
(5) FontDescriptor:字型描述詞典
(6) Encoding:後接的名稱物件或詞典物件為字元編碼用
FontDescriptor字型描述詞典的屬性為:
(1) Type:名稱物件,必須是FontDescriptor
(2) MissingWidth:如果有的話,後接整數物件,內定為0
(3) Descent:如果有的話,後接整數物件,當非0時表示英文字的Descent資訊
其中Descent主要用來調整英文與中文的基準線位置一致,內定為-350。必須特別注意的是,有一些共用的標準字型是不附寬度資訊的(包括FontDescriptor),必須另行特別處理。這些字型的BaseFont名稱為:
Courier
Courier-Bold
Courier-BoldOblique
Courier-Oblique
Helvetica
Helvetica-Bold
Helvetica-BoldOblique
Helvetica-Oblique
Times-Roman
Times-Bold
Times-Italic
Times-BoldItalic
Symbol
ZapfDingbats
當處理這些字型時,必須自備其字寬資訊表。另外,Acrobat 4.0以前的版本,對於與上述字型相容的字型,也是不附字寬資訊表的,這些字型名稱及其對應的標準字型名稱如下:
(1) Arial:Helvetica
(2) Arial,Bold:Helvetica-Bold
(3) Arial,BoldItalic:Helvetica-BoldOblique
(4) Arial,Italic:Helvetica-Oblique
(5) Arial-Bold:Helvetica-Bold
(6) Arial-BoldItalic:Helvetica-BoldOblique
(7) Arial-BoldItalicMT:Helvetica-BoldOblique
(8) Arial-BoldMT:Helvetica-Bold
(9) Arial-Italic:Helvetica-Oblique
(10) Arial-ItalicMT:Helvetica-Oblique
(11) ArialMT:Helvetica
(12) Courier,Bold:Courier-Bold
(13) Courier,Italic:Courier-Oblique
(14) Courier,BoldItalic:Courier-BoldOblique
(15) CourierNew:Courier
(16) CourierNew,Bold:Courier-Bold
(17) CourierNew,BoldItalic:Courier-BoldOblique
(18) CourierNew,Italic:Courier-Oblique
(19) CourierNew-Bold:Courier-Bold
(20) CourierNew-BoldItalic:Courier-BoldOblique
(21) CourierNew-Italic:Courier-Oblique
(22) CourierNewPS-BoldItalicMT:Courier-BoldOblique
(23) CourierNewPS-BoldMT:Courier-Bold
(24) CourierNewPS-ItalicMT:Courier-Oblique
(25) CourierNewPSMT:Courier
(26) Helvetica,Bold:Helvetica-Bold
(27) Helvetica,BoldItalic:Helvetica-BoldOblique
(28) Helvetica,Italic:Helvetica-Oblique
(29) Helvetica-BoldItalic:Helvetica-BoldOblique
(30) Helvetica-Italic:Helvetica-Oblique
(31) TimesNewRoman:Times-Roman
(32) TimesNewRoman,Bold:Times-Bold
(33) TimesNewRoman,BoldItalic:Times-BoldItalic
(34) TimesNewRoman,Italic:Times-Italic
(35) TimesNewRoman-Bold:Times-Bold
(36) TimesNewRoman-BoldItalic:Times-BoldItalic
(37) TimesNewRoman-Italic:Times-Italic
(38) TimesNewRomanPS:Times-Roman
(39) TimesNewRomanPS-Bold:Times-Bold
(40) TimesNewRomanPS-BoldItalic:Times-BoldItalic
(41) TimesNewRomanPS-BoldItalicMT:Times-BoldItalic
(42) TimesNewRomanPS-BoldMT:Times-Bold
(43) TimesNewRomanPS-Italic:Times-Italic
(44) TimesNewRomanPS-ItalicMT:Times-Italic
(45) TimesNewRomanPSMT:Times-Roman
當我們遇到上述字型時,如果取不到字寬資訊,便必須以後面的標準字型字寬資訊代替。
至於Encoding的部份,主要是用來將輸入的字元轉成標準的字元名稱,例如quotesingle表示單引號等。由字元名稱,我們便能解出對應的實際字元碼。Encoding若是名稱物件,表示係使用內定的Encoding方式,可以是MacRomanEncoding、MacExpertEncoding、WinAnsiEncoding、StandardEncoding其中一種(標準字型中的Symbol和ZapfDingbats另有其Encoding方式)。關於這些內定Encoding方式的字碼與字元名稱對應關係,請參見PDF檔格式的相關文件。
如果Encoding是詞典物件,則下面是它的屬性:
(1) Type:後接名稱物件,必須是Encoding
(2) BaseEncoding:如果有的話,後接的名稱物件為本Encoding的基準Encoding方式,必須為MacRomanEncoding、MacExpertEncoding、WinAnsiEncoding、StandardEncoding其中一種,沒有時,TrueType內定為MacRomanEncoding,其餘為StandardEncoding
(3) Differences:如果有的話,後接的陣列物件表示與基準Encoding之間的差異部份
Differences陣列的格式如下:
字碼1 字元名稱11 字元名稱12 … 字碼2 字元名稱21 字元名稱22 …
表示字碼1對應字元名稱11,再下個字碼對應字元名稱12,以此類推。至於各字元名稱對應的實際字碼(Unicode),也請參見PDF檔格式的相關文件(相當長,不適於列於此處)。
b. MMType1
MMType1為Type1字型的擴充,因此其屬性幾乎都完全相同,視為Type1處理即可。
c. TrueType
TrueType字型屬性和Type1也是完全相同的,一樣必須比對是否內定標準字型名稱。另外,這裡的BaseFont還必須比對是否為符號字型,因為符號字的字碼基本上是無意義的。以下是一些符號字型名稱:
Webdings
Wingdins(或Windings 2、Windings 3)
d. Type3
Type1、MMType1、TrueType字型在PDF檔中都只附上寬度相關資訊而已,並沒有實際的字型資料,PDF Viewer必須自行找尋最相似的字型,再運用所附的寬度資訊來加以模擬。然而Type3字型為一種使用者自定字型,除了寬度資訊外,還必須附上字形資料。然而我們需要的,只有寬度資訊而已,單就這部份的屬性而言,大部份都和Type1相同,只是沒有FontDescriptor,因此內定寬度一律為0。另外,寬度單位也不太一樣,我們還需要其他的屬性資訊輔助:
(1) FontMatrix:後接的陣列物件為空間座標轉換矩陣,第一個陣列值即為Width資訊的單位。
e. Type0
Type0字型主要用來支援多位元組的文字資料(例如中文),以下是它的屬性值:
(1) Encoding:可以是內定的CMap名稱,或是一個CMap串流物件
(2) DescendantFonts:後接的字型陣列主要供CMap取得對應字型使用
這部份比較複雜,我們集中在後面一起說明。
11. Unicode字碼、寬度資訊與CMap
對於一個非Type0的字型,輸入字碼一律是一個byte,取得Unicode字碼的方式如下:
(1) 如果有ToUnicode CMap的話,便直接到該CMap取得對應的Unicode字碼(這部份後面會提到)
(2) 如果找不到對應的Unicode字碼,便依輸入字碼到Encoding資訊裡取得該字碼對應的字元名稱
ToUnicode CMap是embedded在pdf檔中的stream(此為優先選項), 若無法由ToUnicode取得, 就必需利用Encoding去取, 取得字碼對應的字元名稱(非type0)或是讀取外部資料得到的CMap(type0)
由於要從PDF檔中取出正確的文字出來,必須基於PDF Writer所附的轉碼表,但早期的PDF檔大都未附(因為那時候只重視原文重現,還未有原文再利用的觀念),在這種情況下取出的文字,一定是亂碼,而且完全無解。這點大家必須牢記在心。
至於取得寬度資訊的方式如下:
(1) 依輸入字碼到Width資訊中取得對應的寬度資訊
(2) 如果沒有對應的寬度資訊,則取內定寬度
Type0字型要取得Unicode字碼與寬度資訊比較麻煩些。由於Type0字型的輸入字碼允許多byte,無論1 byte、2 byte、4 byte都可以,也可以相互混用(只要不衝突即可,例如繁體中文BIG5碼在20h~7Eh便是1 byte,8140h~FEFEh便是2 byte),因此要決定輸入字碼的byte數,便必須利用Encoding屬性裡的CMap資訊。Encoding CMap會將輸入的字碼轉成Font Selector和CID,也就是對應到DescendantFonts字型陣列裡的那個字型與那個字。DescendantFonts陣列裡的字型,存有字型寬度與字集的相關資訊,而且該字型的型態必須是CIDFontType0或CIDFontType2
下面便是這兩種字型型態的屬性:
(1) CIDSystemInfo:後接的詞典資訊用來說明本字型的字集
(2) DW:如果有的話,後接的實數表示內定的寬度(即未定義在W屬性裡字碼應使用的寬度)
(3) W:如果有的話,後接的陣列用來定義各字碼的寬度
CIDSystemInfo的屬性如下:
(1) Registry:後接的字串表示字集類別,例如Adobe
(2) Ordering:後接的字串表示字集名稱,Adobe類別目前有Japan1、Korea1、GB1、CNS1等4種
由於CIDSystemInfo裡記錄了該字型使用的字集,而每種字集的CID都是固定的,因此只要知道這個字集,即可將Encoding CMap轉出來的CID再轉成Unicode字碼(如果有ToUnicode CMap,當然要先由ToUnicode CMap決定)。以目前而言,我們需要製作4份Adobe標準的字集CID轉Unicode字碼表,所幸Acrobat Reader有附這部份的CMap定義檔,名稱後面加-UCS2的就是了,例如Adobe-CNS1-UCS2,因此我們可以將之視為ToUnicode CMap定義檔,直接載入用來轉碼即可(但有版權問題,因此最好還是自行做成轉碼表)。如果沒有ToUnicode CMap,又無法辨識CIDSystemInfo裡的字集,那就無法轉換出正確的Unicode字碼了。
至於W的陣列內容格式有下列兩種:
起始CID 寬度陣列
起始CID 結束CID 寬度
第一種格式表示由該CID開始的寬度,都定義在寬度陣列中,數目由寬度陣列決定。第二種格式表示在該範圍的CID都是使用後面所指示的寬度。
綜合一下,Type0字型取得Unicode字碼的方式如下:
(1) 如果有ToUnicode CMap的話,便直接到該CMap取得對應的Unicode字碼
(2) 如果找不到對應的Unicode字碼,便利用Encoding CMap將輸入字碼轉成Font Selector和CID
(3) 依Font Selector到DescendantFonts取出對應的CID字型,由其CIDSystemInfo決定字集名稱,再依該字集名稱的CID轉Unicode字碼表轉換成Unicode字碼
取得寬度資訊的方式如下:
(1) 利用Encoding CMap將輸入字碼轉成Font Selector和CID
(2) 依Font Selector到DescendantFonts取出對應的CID字型,再到該字型依CID取得寬度資訊,如果沒有寬度資訊,則使用內定寬度資訊
如此全部字型的Unicode字碼與寬度資訊取法都已明朗化了,剩下的便只有Encoding CMap和ToUnicode CMap這兩個而已。以下便開始說明這兩個CMap。
Encoding CMap可以是一個名稱物件,或是一個串流物件,如果是一個名稱物件,便表示為內定的Encoding CMap。PDF檔定義了許多常用內定的Encoding CMap,其目的當然便是要減少PDF檔案的大小。至於有那些內定的Encoding CMap,可以直接到Acrobat Reader程式目錄裡找CMap目錄,裡面便有一堆(且隨著版本更新而增加)。因此我們的做法最好也是附上這些檔案,然後在遇到標準的Encoding CMap名稱時,再去找對應相同名稱的CMap定義檔,讀取其CMap定義。
其中有2個比較特別的定義:Identity-H和Identity-V,這兩個都是直接將2 byte輸入字碼視為CID處理(high byte在前)。雖然CMap目錄裡也有附這兩個標準CMap定義檔,但由於很常見,故為了速度考量,可以直接另行處理。
如果Encoding CMap不是一個內定的Encoding CMap名稱,則必須以串流物件來定義它,下面便是串流物件CMap的重要屬性:
(1) Type:後接名稱物件,必須是CMap
(2) UseCMap:如果有的話,後接的CMap(可能是名稱物件或串流物件),係為本CMap資料的基準,其後接的串流資料,便是用來修改此一基準,成為本CMap的實際內容
(3) WMode:如果有的話,後接的數字若是1表示為直排字型,內定為0(會影響排字走向)
雖然在串流資料裡定義的CMap內容,必須符合“Adobe CMap and CIDFont File Specification”裡定義的語法,不過由於Encoding CMap和ToUnicode CMap的格式不太一樣,我們還是分別予以說明。Encoding CMap的格式如下(只取相關部份,其餘均可略過,標準Encoding CMap定義檔的內容也是相同形式):
… (略)
begincmap
基準名稱 usecmap
cmap檔頭資訊(不重要,可略過)
範圍定義(不重要,可略過)
轉碼定義*
endcmap
… (略)
若有usecmap表示做為基準的內定Encoding CMap名稱,後面的定義是用以修改此基準定義的資料。轉碼定義的格式如下:
字型索引 usefont
定義項數 begincidrange
範圍轉碼定義*
endcidrange
字型索引便是Font Selector,用來表示其後轉出的CID是對應到那個CID字型(由0編起)。如果省略的話,則沿用前面的定義,如果都沒有,則內定為0。範圍轉碼定義的形式如下:
開始字碼 結束字碼 CID
表示輸入的起迄字碼範圍,剛好對應到後面CID開始的數字。注意開始字碼/結束字碼的格式為字串物件,而CID是數字物件。字串的長度同時也表示了輸入字碼的長度,例如:
<20>表示輸入字碼只需一個byte
<0020>表示輸入字碼要2個byte0020>20>
至於ToUnicode CMap,它本身必須是一個串流資料(因為沒有內定的ToUnicode CMap,故串流物件裡的UseCMap屬性也不能是名稱物件),其格式如下:
… (略)
begincmap
cmap檔頭資訊(不重要,可略過)
範圍定義(不重要,可略過)
轉碼定義*
endcmap
… (略)
轉碼定義的格式如下:
定義項數 beginbfrange
範圍轉碼定義*
endbfrange
或是
定義項數 beginbfchar
字元轉碼定義*
endbfchar
範圍轉碼定義有下列兩種形式:
開始字碼 結束字碼 對應的開始Unicode字串
開始字碼 結束字碼 [各字碼應轉換的Unicode字串]
第一種形式表示符合該字碼範圍的文字,剛好線性地對應到Unicode字串開始的一個連續區。第二種形式表示符合該字碼區的文字,必須到後面的字串陣列中取得對應的Unicode字串。至於字元轉碼定義,則是:
字碼 該字碼應轉換的Unicode字串
這邊必須注意的是,轉換後的Unicode字串可能有多個Unicode字元(無論任何轉碼定義形式),例如:
<005f8192> <00660069>00660069>005f8192>
表示當輸入字碼符合00h,5Fh,81h,92h時(4 byte),便將它轉成0066h、0069h這兩個Unicode字碼(也就是fi)。
12. 加密的PDF檔
在trailer資料中若有Encrypt屬性,表示該PDF檔的內容有加密。加密的對象是所有在參用物件裡的字串資料與串流資料(trailer區塊內容除外,至於串流資料裡的頁描述命令之字串資料,也不再二次加密)。Encrypt屬性值是一個詞典物件,下面便是該詞典的屬性:
(1) Filter:後接的名稱物件表示加密方式,Standard表示內定方式
(2) R:後接的數字表示該加密方式的修正版本,目前Standard的修正版本為2或3
(3) O:後接的字串表示擁有者密碼(有加密,固定32 byte,後述)
(4) U:後接的字串表示使用者密碼(有加密,固定32 byte,後述)
(5) P:後接的數字表示存取權限值
(6) Length:如果有的話,後接的數字表示加密基礎鍵值位元數,內定為40(不能>128,即16 byte)
(7) V:如果有的話,後接的數字表示加密演算法版本,目前必須為1或2,內定為0(即舊版已註銷的方式)
PDF檔現行Standard的加密方法,是採用MD5 Hashing函數來製作鍵值,然後採用RC4來進行加密編碼。MD5 Hashing函數可接受任何長度的資料,然後輸出固定的16 byte資料;RC4則可接受任何長度的鍵值,然後對輸入的字元進行加密編碼。由於這兩個演算法都是國際標準,因此這邊便不再做詳細的說明。
由於前述的加密方式,主要取決於鍵值的製作方式,因此首先說明PDF檔解密使用的鍵值取得方式:
(1) 將輸入密碼字串取32 byte做為MD5函數的輸入資料,不足時需補上0x28, 0xbf, 0x4e, 0x5e, 0x4e, 0x75, 0x8a, 0x41, 0x64, 0x00, 0x4e, 0x56, 0xff, 0xfa, 0x01, 0x08, 0x2e, 0x2e, 0x00, 0xb6, 0xd0, 0x68, 0x3e, 0x80, 0x2f, 0x0c, 0xa9, 0xfe, 0x64, 0x53, 0x69, 0x7a等字元,補滿為止
(2) 再取Encrypt詞典裡加密的擁有者密碼字串(32 byte),做為MD5函數的輸入資料
(3) 再取Encrypt詞典裡的存取權限值,視為4 byte資料(低位元組先),做為MD5函數的輸入資料
(4) 再取trailer區塊中的ID屬性值(字串陣列)的第一個ID字串,做為MD5函數的輸入資料
(5) 開始進行MD5 Hashing運算
(6) 如果是修正版本3,則取MD5 Hashing運算結果再進行MD5 Hashing運算,如此重複50次
(7) 取運算結果最前面所需的位元組數(依Length屬性值而定),做為解密鍵值
然而PDF檔裡有擁有者密碼和使用者密碼兩種,上述步驟裡的密碼到底是那一種呢?答案是使用者密碼。然而當輸入的是擁有者密碼時,要如何取得鍵值呢?事實上,Encrypt詞典裡的擁有者密碼和使用者密碼都已經不是原來的資訊了,這是為了避免被別人破解,造成無需輸入密碼即可閱讀文件內容所設。PDF檔裡的使用者密碼,記錄的只是用來比對使用者密碼的正確性而已,並無法還原成原來的使用者密碼。而PDF檔裡的擁有者密碼,則是記錄著供取得鍵值的使用者密碼資訊(即上述步驟1的32 byte字串),當然這部份是有另行加密的。因此當輸入的是擁有者密碼時,必須先將O屬性值解密成32 byte使用者密碼資訊,再送入上述步驟取得解密鍵值。由於輸入密碼的正確與否,關鍵著解密鍵值的正確與否(否則無法解密PDF文件內容),因此一定要去比對U屬性值的正確性。以下是將O屬性值解密成32 byte使用者密碼資訊的方法:
(1) 將輸入的擁有者密碼依前述步驟1做成32 byte字串,送入MD5函數
(2) 如果是修正版本3,則將運算結果再送入MD5運算50次
(3) 取運算結果最前面所需的位元組數(依Length屬性值而定),做為解密鍵值
(4) 利用該解密鍵值,將O屬性值送入RC4解密,結果即為32 byte使用者密碼資訊
至於檢驗使用者密碼正確與否的方法,則依修正版本而定。修正版本2的方式為:
(1) 依之前的步驟取得解密鍵值(輸入的若是32 byte使用者密碼資訊,則可省略步驟1)
(2) 利用該解密鍵值,將U屬性值送入RC4解密,得到32 byte字串
(3) 比對32 byte字串結果與原用來取得解密鍵值的32 byte使用者密碼資訊,若相同便是正確
修正版本3的方式為:
(1) 依之前的步驟取得解密鍵值(輸入的若是32 byte使用者密碼資訊,則可省略步驟1)
(2) 將32 byte使用者密碼資訊,以及trailer區塊中的ID屬性值第一個ID字串送入MD5運算,得到檢驗值
(3) 設定目前運算結果為U屬性值,然後重複步驟4-5計20次,比對最終結果的前16 byte是否和步驟2的檢驗值相同,若相同便是正確
(4) 將步驟1的解密鍵值每個位元組與剩餘次數(0-19)進行互斥運算,做為新的解密鍵值
(5) 利用該新的解密鍵值,將目前運算結果送入RC4解密,得到新的運算結果
當使用者密碼確認正確時,便表示前面取到的解密鍵值是正確的,可以用來對PDF檔文件內容進行解密(字串物件與串流物件)。解密的方式如下:
(1) 取解密鍵值,以及物件所在的物件編號最低3 byte與代數2 byte(低元組先),送入MD5運算,取與送入資料長度等長的運算結果做為新的解密鍵值(若超過16 byte則以16 byte為準)
(2) 利用該新的解密鍵值,將字串資料或串流資料送入RC4解密,得到解密後的字串資料或串流資料
勘誤:
>至於檢驗使用者密碼正確與否的方法,則依修正版本而定。修正版本2的方式為:
>(1) 依之前的步驟取得解密鍵值(輸入的若是32 byte使用者密碼資訊,則可省略步驟1)
>(2) 利用該解密鍵值,將U屬性值送入RC4解密,得到32 byte字串
>(3) 比對32 byte字串結果與原用來取得解密鍵值的32 byte使用者密碼資訊,若相同便是正確
(3) 比對32 byte字串結果與與前述32 byte密碼附加資料,若相同便是正確
>修正版本3的方式為:
>(1) 依之前的步驟取得解密鍵值(輸入的若是32 byte使用者密碼資訊,則可省略步驟1)
>(2) 將32 byte使用者密碼資訊,以及trailer區塊中的ID屬性值第一個ID字串送入MD5運算,得到檢驗值
>(3) 設定目前運算結果為U屬性值,然後重複步驟4-5計20次,比對最終結果的前16 byte是否和步驟2的檢驗值相同,若相同便是正確
>(4) 將步驟1的解密鍵值每個位元組與剩餘次數(0-19)進行互斥運算,做為新的解密鍵值
>(5) 利用該新的解密鍵值,將目前運算結果送入RC4解密,得到新的運算結果
(2) 將32 byte密碼附加資料,以及trailer區塊中的ID屬性值第一個ID字串送入MD5運算,得到檢驗值
PDF 1.2版
1.PDF的檔頭
1.2版的檔頭可能存放了額外資訊,因此%PDF-1.2的字串會從80h位置開始,其餘部份均同PDF 1.3版的格式,除了所有與檔案位置有關的部份,皆偏移了80h。亦即,去除了檔頭的128 byte資料,才是真正的PDF檔資料。
PDF 1.5版
1.xref table區塊
在PDF 1.5裡,允許了stream方式來定義xref table,因此在讀取xref table區塊資料時,若startxref指向的不是xref table,而是一個stream object,則必須將之解開來。由於未得到xref table資料前,無法取得間接物件的資料,因此裡面的資料都必須是直接格式,而不能是間接格式。又解壓格式亦有增加,請參閱後述stream資料的解壓方法一節。
解開該stream object後,其dictionary object便是trailer,stream內容則是xref table,但其格式有些不同。首先取得Index屬性,這是一個整數陣列,每2個整數一組,分別表示物件開始編號與參照表項目數(請參閱1.3版的xref table說明)。如果省略,則一律以物件編號0開始,參照表項目數同Size屬性值(數字)。
接下來便是讀取W屬性,這是一個整數陣列,固定為3個整數,各代表3個欄位值所佔的byte數(高位元組在前)。第一個欄位值表示其後兩個欄位值的形態,若W[0]的值是0,表示不記錄第一個欄位值,全數內定為Type 1,W[1]或W[2]的值若為0,則以內定值為準。各形態的意義如下:
Type 0:可用物件的串列,欄位2值=下個可用物件編號,欄位3值=重用該可用物件時應使用的代數。
Type 1:未壓縮的物件,欄位2值=資料所在的檔案位置,欄位3值=代數,內定為0。
Type 2:壓縮的物件,欄位2值=本物件資料所存放的stream資料所在物件編號(代數固定為0),欄位3值=本物件在該stream資料物件的索引值。
2.物件的形態與參用
壓縮物件所在的Stream物件參數如下:
(1)Type:固定為ObjStm
(2)N:stream裡存放的物件數
(3)First:第一個物件資料在stream資料的相對位置
stream資料內容為:
{物件編號 物件資料相對於第一物件資料的位置}* {物件資料}*
因此一開始必須有N*2個數字物件。物件資料,不含obj與endobj等識別字串。壓縮物件的代數,固定為0。
3. stream資料的解壓方法
(1) LZWDecode
(2) FlateDecode
允許Predictor參數屬性值>1:
2 TIFF Predictor 2
10 PNG prediction (on encoding, PNG None on all rows)
11 PNG prediction (on encoding, PNG Sub on all rows)
12 PNG prediction (on encoding, PNG Up on all rows)
13 PNG prediction (on encoding, PNG Average on all rows)
14 PNG prediction (on encoding, PNG Paeth on all rows)
15 PNG prediction (on encoding, PNG optimum)
可參考RFC 2083: PNG (Portable Network Graphics) Specification。裡面的Columns參數屬性值為每行的byte數,PNG Predictor各行第一個byte為演算法編號:
0 = PNG None
1 = PNG Sub
2 = PNG Up
3 = PNG Average
4 = PNG Paeth
5 = PNG optimum
PDF 1.6版
1. 加密的PDF檔
PDF1.6版引入了AES加密法,同時在Encrypt詞典物件,新增了一個CF屬性(當R=4且V=4時才有)。CF是一個詞典物件,裡面主要是看兩個屬性:
(1) StrF:String字串資料的加密法
(2) StmF:Stream串流資料的加密法
StrF和StmF的格式是相同的,我們合併一起說明。
(1) 如果是一個名稱物件,且名稱為Identity,表示不加密。
(2) 如果是一個詞典物件,則找CFM屬性值,這必須是一個名稱物件,若名稱是V2,表示採用RC4加密,若名稱是AESV2,表示是AES加密。另,Length屬性值則是鍵值byte數,AES加密的話,固定是16(即128 bit)。
若採用RC4加密,跟1.3版的方法完全相同。若採用AES加密的話,密碼驗證與解密鍵值的計算方式皆相同,但在資料解密時有些不同:
(1) 取解密鍵值,以及物件所在的物件編號最低3 byte與代數2 byte(低元組先),送入MD5運算
(2) 若為AES加密,則再送入"sAlT"這個字串(4 byte)
(3) 取運算結果中,取解密鍵值長度+5 byte的資料做為新的解密鍵值(若超過16 byte則以16 byte為準)
(4) 利用該新的解密鍵值,將字串資料或串流資料送入RC4解密,得到解密後的字串資料或串流資料
(5) 若為AES,則以該新的解密鍵值為解密鍵值,字串資料或串流資料前16 byte為初始向量(IV),對字串資料或串流資料16 byte以後的資料進行AES CBC解密(每個Block為16 byte),所得資料為PKCS #5格式,將解密的資料長度減去最後一個byte的值,便是實際的資料長度
PDF 1.6文件中,並未提到第(2)點,亦未提到AES加密資料為PKCS#5格式,會造成解密錯誤的盲點,請特別注意。
關於PDF Password Cracker:
其實PDF加密文件本身並不保密, 原因出在解密鍵值計算, 以及密碼核驗的程序, 本身其實並不必花太多時間. 因此可以採用嘗試錯誤法, 來比對出真正的密碼. 其過程為:
1.產生一個可能的密碼
2.計算解密鍵值
3.核驗密碼正確性, 不對便回到步驟1.
假設密碼可用字元共有n個, 則這項嘗試次數<=n^32 (PDF密碼長度最多32字元). 由於一般人使用的密碼, 幾乎很少超過8個字元, 因此絕大部份情況, 在嘗試n^8次後, 便可以破解出密碼了.