雖然這與Boost無關,但請允許我向您保證,PDF(和PostScript)的解析只是您可能想要的微不足道。假設您有一個可以返回一系列令牌的掃描器對象。令牌的類型,你會從掃描儀得到的是:
- 字符串
- 字典開始(< <)
- 快譯通結束(>>)
- 名稱(/其他)
- 數
- 十六進制陣列
- 左角(<)
- 右角(>)
- 陣列開始([)
- 陣列端(])
- 過程開始({)
- 程序端(})
- 評論(%富)
- 字
我的掃描儀是一個有限狀態自動機,具有開始,註釋,字符串,十六進制數組,標記,DictEnd和完成狀態。
解析PDF的方式不是通過解析它,而是通過執行它。鑑於這些令牌,我的「語法分析器」看起來像這樣(在C#):
while (true) {
MLPdfToken = scanner.GetToken();
if (token == null)
return MachineExit.EndOfFile;
PdfObject obj = PdfObject.FromToken(token);
PdfProcedure proc = obj as PdfProcedure;
if (proc != null)
{
if (IsExecuting())
{
if (token.Type == PdfTokenType.RBrace)
proc.Execute(this);
else
Push(obj);
}
else {
proc.Execute(this);
}
if (proc.IsTerminal)
return Machine.ParseComplete;
}
else {
Push(obj);
}
}
我還要補充一點,如果你給每個PdfObject的execute()方法,使得基類的實現是machine.Push(this)
和IsTerminal
那返回false
,在REPL變得更容易:
while (true) {
MLPdfToken = scanner.GetToken();
if (token == null)
return MachineExit.EndOfFile;
PdfObject obj = PdfObject.FromToken(token);
if (IsExecuting())
{
if (token.Type == PdfTokenType.RBrace)
obj.Execute(this);
else
Push(obj);
}
else {
obj.Execute(this);
if (obj.IsTerminal)
return Machine.ParseComplete;
}
}
有更多的支持在機 - 機具有PdfObject的堆棧和訪問它的一些方法(PUSH,POP,馬克,CountToMark,指數,Dup的,掉期)以及ExecProcBegin和ExecProcEnd。
除此之外,它非常輕。唯一有點奇怪的是,PdfObject.FromToken
需要一個標記,如果它是一個基本類型(數字,字符串,名稱,十六進制,布爾)返回一個相應的PdfObject。否則,它會使用給定的標記並查找與「PdfProcedure
」對象關聯的過程名稱的「proc set」字典。因此,當你遇到令牌<<
是被在該PROC集擡頭一看,這個代碼出現:
void DictBegin(PdfMachine machine)
{
machine.Push(new PdfMark(PdfMarkType.Dictionary));
}
所以<<
真正的意思是「標記堆棧作爲字典開始>>
變得更有趣:
void DictEnd(PdfMachine machine)
{
PdfDict dict = new PdfDict();
// PopThroughMark pops the entire stack up to the first matching mark,
// throws an exception if it fails.
PdfObject[] arr = machine.PopThroughMark(PdfMarkType.Dictionary);
if ((arr.Length & 1) != 0)
throw new PdfException("dictionaries need an even number of objects.");
for (int i=0; i < arr.Length; i += 2)
{
PdfObject key = arr[i], val = arr[i + 1];
if (key.Type != PdfObjectType.Name)
throw new PdfException("dictionaries need a /name for the key.");
dict.put((PdfName)key, val);
}
machine.Push(dict);
}
所以>>
彈出到最近的字典標記到一個數組,然後把每一對到字典中。現在,我可以這樣做不分配的數組。我可以只流行對,將它們放入字典直到我達到標記,沒有得到一個名字或下溢堆棧。
重要的是,PDF中沒有任何語法,在PostScript中也沒有。至少不會像你會注意到的那麼多。唯一真正的語法(和read-eval-(push)循環顯示它)是'}'。
所以,當你這是一個PDF 14 0 obj << /Type /Annot /SubType /Square >> endobj
什麼你真正看到的是一系列的程序:
- 推14
- 推0
- 執行OBJ(POP兩個數字並按下「定義」目的)。
- 執行字典開始
- 推/類型
- 推/阿諾
- 推/子
- 推/廣場
- 執行詞典結束
- 執行endobj(彈出頂部對象,然後得到(如果第二個是定義,則將其「值」設置爲第一個對象,否則拋出)。
由於「endobj」是終端,解析結束,堆棧的頂部是結果。
因此,當您被要求在PDF中查找對象14時,交叉引用表會告訴您在哪裏尋找,您在該位置創建一個帶有流指針的新機器並運行它。如果堆棧的頂部是一個「定義」對象,那麼你已經成功了。
關於現在你應該點頭,但不信任我,因爲你考慮PDF流,這是這樣的:
<< [/key value]* >> stream ...raw data... endstream endobj
同樣,沒有語法。 proc stream
看着堆棧的頂部,它應該是一個PdfDict。如果是,它會消耗字符,直到下一個換行符(掃描程序執行此操作),將當前文件位置存儲在流中作爲數據開始,從字典中讀取流長度(這可能會導致另一臺機器變得更新),並跳過超過流的末尾並將新的流對象推入堆棧。 endstream是一個沒有操作。一個PdfDict和一個PdfStream之間的唯一區別是PdfStream有一個開始位置和一個布爾說它是一個流,否則我是雙重目的的對象。
除了執行環境稍微複雜之外,PostScript幾乎完全相同。例如,您的機器需要多個堆棧:參數堆棧,字典堆棧和執行堆棧。從那裏,你或多或少地將你的標記器綁定到原始程序集以及單詞exec中,然後你的大多數解釋器都是用PS自己寫的。
如果你在談論boost,那麼你在看C++,這意味着你不能像我一樣像內存一樣快速和鬆散,所以你會想要使用智能指針或者弄清楚在那裏你的範圍是和小心處置對象,而不是輕輕地扔掉它們,但這只是正常的C++的東西。
目前,我在.NET中爲我的公司製作了PDF工具,但在過去的一段時間裏,我從事的是Acrobat 1-4版本,我所描述的大部分內容正是Acrobat在引擎蓋下所做的(或者更多更少 - 這是C,而不是C++,但它是相同的方法)。
對於外部參照表(或外部參照流),您首先閱讀 - 規範告訴您,如果您跳轉到EOF並回掃,就會找到外部參照表的開始。你解析(這是一個CS 101的任務),解析預告片,尋找到/ Prev(如果有的話)並重復,直到沒有更多的/ Prev條目。這爲您提供了查看對象的完整外部參照。
至於寫作 - 您可以採取多種方法。最明顯的是,當一個對象被引用時,通過爲其分配最新的可用外部參照條目來創建一個新的引用對象。每當對象引用其他對象進行寫入時,他們都會詢問是否引用了這些對象。如果他們是,他們寫的參考(即,14 0 R
)。當需要編寫一個被引用的對象時,你會得到當前流指針並將其存儲在外部參照中,然後寫入<objnum> <generation> obj <object contents> endobj
。例如,我的代碼來寫一本字典是這樣的:
public override ToStream(PdfStreamingContext context)
{
if (context.HasReference(this)) // is object referenced in xref
{
PdfUtils.WriteObjectDefinitionBegin(this, context);
}
context.Writer.Indent();
context.Writer.WriteLine("<<");
WriteContents(context);
context.Writer.Exdent();
context.Writer.Writeline(">>");
if (context.HasReference(this))
{
PdfUtils.WriteObjectDefinitionEnd(this, context);
}
}
我砍傷了一些糠,所以你可以看到下面的小麥。上下文是一個對象,它持有一個新的外部參照表以及一個寫入流的對象,該對象自動處理適當的新行紀律,縮進,換行等。
你應該看到的是,如果不是微不足道的話,這裏的基礎知識是直截了當的。現在,當你應該問自己這樣一個問題時,「如果這個問題微不足道,那麼在市場上怎麼會沒有更多(嚴重)的Acrobat競爭呢?答案是,即使它很微不足道,編寫PDF文件仍然很容易符合規範並且Acrobat處理其中的大多數,真正的挑戰是能夠遵守規範並確保將所有必需的值包含在字典中,並且它們處於範圍和語義上正確的地獄,甚至日期時間格式 - 這是很好的規定 - 是我的圖書館中的一個特殊情況代碼堆,用於管理其他人將其嚴重破壞的地方。能夠始終生成正確的PDF格式非常困難並且消耗了大量PDF中的垃圾在世界上更難
我可以(也可能應該)寫一本關於如何做到這一點的書,雖然很多邊緣代碼是骯髒的,但整體結構尿可以很漂亮。
tl; dr - 如果您正在考慮使用PDF的遞歸下降解析器,那麼您的想法太難了。所有你需要的是一個標記器和一個簡單的REPL。
我已經得到了一級掃描器的C代碼(沒有<<>>)[這裏](https://groups.google.com/d/msg/comp.lang.postscript/XbxHv5rcFxc/OetXbfI4PQYJ)和部分翻譯爲postscript [here](https://groups.google.com/d/msg/comp.lang.postscript/u4QmuQZhrxU/LNF_r0PWX1EJ)。 – 2013-04-11 21:02:10
找到另一個(在postscript中)[這裏](https://groups.google.com/d/msg/comp.windows.news/g1fs5ajR1YQ/FgW3DFKx0dUJ)。 – 2013-04-13 06:01:15