提升精神可以处理类似语言的Postscript/PDF吗?

use*_*984 5 pdf parsing postscript boost-spirit lexer

我注意到Boost精神提供了一些限制,在这里的一个问题上有一个用户请求有关提升精神的帮助,另一个用户给出了答案指定,提升精神适用于语句而不是"通用文本"(I对不起,如果我没记错的话.

现在我想用令牌来考虑Postscript和PDF,并以这种方式简化我对这种格式的处理方式,问题在于PDF是标记语言和编程语言之间的混合,其中包含跳转和表格,在考虑最流行的文件格式(如XML,C++代码和其他语言和格式)时,我无法想到类似的东西.

还有另外一个事实:我真的找不到那些在使用boost :: spirit wiriting pdf解析器或编写器方面有经验的人,所以我问,boost :: spirit它能够解析PDF文件和输出作为代币的元素?

pli*_*nth 11

虽然这与Boost无关,但请允许我向您保证,PDF(和PostScript)的解析与您想要的一样简单.假设您有一个返回一系列标记的扫描仪对象.您将从扫描仪获得的令牌类型是:

  • Dict开始(<<)
  • 字典结束(>>)
  • 姓名(/等等)
  • 十六进制数组
  • 左角(<)
  • 直角(>)
  • 数组开始([)
  • 数组结束(])
  • 程序开始({)
  • 程序结束(})
  • 评论(%foo)

我的扫描仪是一个有限状态自动机,具有Start,Comment,String,HexArray,Token,DictEnd和Done的状态.

解析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);
    }
}
Run Code Online (Sandbox Code Playgroud)

我还要补充一点,如果你给每个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;                
    }
}
Run Code Online (Sandbox Code Playgroud)

机器中有更多的支持 - 机器有一堆PdfObject和一些访问它的方法(Push,Pop,Mark,CountToMark,Index,Dup,Swap),以及ExecProcBegin和ExecProcEnd.

除此之外,它非常轻.唯一有点奇怪的是PdfObject.FromToken获取一个令牌,如果它是一个原始类型(数字,字符串,名称,十六进制,布尔)返回相应的PdfObject.否则,它接受给定的令牌并查找与PdfProcedure对象关联的过程名称的"proc set"字典.因此,当您遇到在<<proc集中查找的令牌并提出此代码时:

void DictBegin(PdfMachine machine)
{
    machine.Push(new PdfMark(PdfMarkType.Dictionary));
}
Run Code Online (Sandbox Code Playgroud)

所以<<真的意味着"将堆栈标记为字典的开头.>>更有趣:

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);
}
Run Code Online (Sandbox Code Playgroud)

因此>>弹出最近的字典标记到一个数组然后将每一对放入字典.现在,我可以在不分配数组的情况下完成此操作.我可以弹出对,将它们放入字典中,直到我达到标记,无法获得名称或使堆栈下溢.

重要的一点是,PDF中没有任何语法,PostScript也没有.至少没有你注意到的那么多.唯一真正的语法(和read-eval-(push)循环显示它)是'}'.

所以当你这是一个PDF时14 0 obj << /Type /Annot /SubType /Square >> endobj,你真正看到的是一系列程序:

  1. 推14
  2. 按0
  3. 执行obj(弹出两个数字并按下"定义"对象).
  4. 执行字典开始
  5. 推/类型
  6. 推/无
  7. 推/子类型
  8. 推/广场
  9. 执行字典结束
  10. 执行endobj(弹出顶部对象,然后获取(不弹出)下一个.如果第二个是定义,则将其"value"设置为第一个对象,否则抛出).

由于"endobj"是终端,解析结束并且堆栈的顶部是结果.

因此,当要求您在PDF中查找对象14时,交叉引用表会告诉您在哪里寻找,您在该位置创建一个带有流指针的新机器并运行它.如果堆栈的顶部是"定义"对象,那么您已经成功了.

现在你应该点头但不信任我,因为你正在考虑PDF流,如下所示:

<< [/key value]* >> stream ...raw data... endstream endobj
Run Code Online (Sandbox Code Playgroud)

同样,没有语法.proc stream查看堆栈的顶部,它应该是PdfDict.如果是,它会消耗字符直到下一个换行符(扫描仪执行此操作),将当前文件位置存储在流中作为数据开始,从dict读取流长度(这可能导致另一台机器被新建),并跳过超过流的末尾并将新流对象推送到堆栈上.endstream是一个无操作.PdfDict和PdfStream之间唯一的区别是PdfStream有一个起始位置,bool说它是一个流,否则我是对象的双用途.

PostScript几乎完全相同,只是执行环境稍微复杂一些.例如,您的机器中需要多个堆栈:参数堆栈,字典堆栈和执行堆栈.从那里,你或多或少只是将你的标记化器绑定到一组基本过程以及单词exec,然后你的大部分解释器都是用PS本身编写的.

如果你在谈论boost,那你就是在看C++,这意味着你不能像我一样快速和松散地使用内存,所以你要么想要使用智能指针,要么找出你的范围是什么并小心处理对象而不是轻率地抛弃它们,但这只是正常的C++内容.

目前,我在.NET中为我的公司制作PDF工具,但在以前的生活中我使用的是Acrobat版本1-4,而我所描述的大部分内容正是Acrobat在幕后所做的(好吧,或多或少 - 它是C,而不是C++,但它是相同的方法).

关于外部参照表(或外部参照流),您首先阅读 - 规范告诉您如果跳转到EOF并向后扫描,则会找到外部参照表的开头.你解析它(这是一个CS 101分配),解析预告片,寻找/上一个(如果有的话)并重复直到没有更多/上一个条目.这为您提供了一个用于查找对象的完整外部参照.

至于写作 - 你可以采取多种方法.最明显的一点是,当要引用一个对象时,可以通过为其指定最新的可用外部参照条目来创建新的引用对象.每当对象引用其他对象进行写入时,它们会询问是否引用了这些对象.如果是,则编写参考(即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);
    }
}
Run Code Online (Sandbox Code Playgroud)

我已经砍掉了一些谷壳,所以你可以看到下面的小麦.上下文是一个对象,它包含一个新的外部参照表以及一个用于写入流的对象,这些流可以自动处理适当的换行规则,缩进,换行等.

你应该看到的是,这里的基础知识是直截了当的,如果不是微不足道的话.而现在你应该问自己一个问题,"如果它是微不足道的,为什么市场上没有更多(严重)的Acrobat竞争?答案是,即使它是微不足道的,它仍然很容易写出不是符合规范,Acrobat处理其中的大多数.真正的挑战是能够遵守规范,并确保在字典中包含所有必需的值,并且它们在范围和语义上是正确的.地狱,甚至是日期时间格式 - 这是非常明确的 - 是我的库中的一大堆特殊情况代码,用于管理其他人在王室中搞砸了它的地方.能够生成一致正确的PDF很难并消耗PDF格式的垃圾在世界上更难.

我可以(也可能应该)写一本关于如何做到这一点的书.虽然很多边缘代码都是肮脏的,但总体结构可能非常漂亮.

tl; dr - 如果您正在考虑PDF的递归下降解析器,那么您的想法太难了.您只需要一个tokenizer和一个简单的REPL.