2013-04-08 65 views
5

我注意到Boost精神提供了一些限制,在这里的一个问题有一个用户寻求关于助推精神的帮助,而另一个给出答案的用户指定了助推精神与陈述很好地配合,而不是“通用文本” (如果我没有正确记得,我很抱歉)。Boost精神可以处理Postscript/PDF之类的语言吗?

现在我想从标记的角度思考Postscript和PDF,并简化我对这种格式的处理方式,问题在于PDF是一种标记语言和具有跳转的编程语言表格,在考虑XML,C++代码和其他语言和格式等最流行的文件格式时,我无法想到类似的东西。还有另外一个事实:我无法真正找到具有某种经验的人员,他们对boost :: spirit提供了一种pdf解析器或作者的引导,所以我问,boost :: spirit它能够解析PDF文件并输出元素作为令牌?

+0

我已经得到了一级扫描器的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

+0

找到另一个(在postscript中)[这里](https://groups.google.com/d/msg/comp.windows.news/g1fs5ajR1YQ/FgW3DFKx0dUJ)。 – 2013-04-13 06:01:15

回答

11

虽然这与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什么你真正看到的是一系列的程序:

  1. 推14
  2. 推0
  3. 执行OBJ(POP两个数字并按下“定义”目的)。
  4. 执行字典开始
  5. 推/类型
  6. 推/阿诺
  7. 推/子
  8. 推/广场
  9. 执行词典结束
  10. 执行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。

+0

这看起来非常有趣,有两件事情我不清楚:用你的“逐行阅读+需要时跳跃”来处理你如何管理'xref'部分?如何编写PDF?当你需要逐行写入而不跳跃时,如何管理写一个? – user2244984 2013-04-09 05:46:25

+0

+1优秀的答案,完全同意。我喜欢精神,但不会在这里使用它(好吧,也许是为了与Spirit Lex共舞)。 @ user2244984对于编写,您将始终具有(并且需要)内存表示以遍历输出流顺序。 – sehe 2013-04-09 11:55:56