5

我注意到 Boost Spirit 提供了一些限制,在 SO 上的一个问题中,有一个用户寻求有关 boost spirit 的帮助,而另一个给出答案的用户指定 boost spirit 适用于语句而不是“通用文本”(我'对不起,如果我没有记错的话)。

现在我想从标记的角度考虑 Postscript 和 PDF,并以这种方式简化我对这种格式的处理方法,问题是 PDF 是一种标记语言和一种带有跳转和表格的编程语言之间的混合体,在考虑最流行的文件格式(如 XML、C++ 代码和其他语言和格式)时,我想不出类似的东西。

还有另一个事实:我真的找不到有过 boost::spirit 编写 pdf 解析器或编写器经验的人,所以我问 boost::spirit 它能够解析 PDF 文件并输出元素作为标记?

4

1 回答 1

11

尽管这与 Boost 无关,但让我向您保证,PDF(和 PostScript)的解析与您希望的一样简单。假设您有一个返回一系列令牌的扫描仪对象。您将从扫描仪获得的令牌类型是:

  • 细绳
  • 字典开始 (<<)
  • 字典结尾 (>>)
  • 姓名(/随便)
  • 数字
  • 十六进制数组
  • 左角 (<)
  • 直角 (>)
  • 数组开始 ([)
  • 数组结束(])
  • 过程开始 ({)
  • 程序结束 (})
  • 评论 (%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);
    }
}

我还要补充一点,如果你给每个 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;                
    }
}

Machine 有更多支持 - Machine 有一堆 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));
}

所以<<真正的意思是“将堆栈标记为字典的开头。>>变得更有趣:

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(弹出两个数字并推送一个“定义”对象)。
  4. 执行字典开始
  5. 推/打字
  6. 推/注
  7. 推送/子类型
  8. 推/方
  9. 执行字典结束
  10. 执行 endobj(弹出顶部对象,然后获取(不弹出)下一个对象。如果第二个是定义,则将其“值”设置为第一个对象,否则抛出)。

由于“endobj”是终端,解析结束,栈顶就是结果。

因此,当您被要求在 PDF 中查找对象 14 时,交叉引用表会告诉您要查找的位置,您使用该位置的流指针创建一个新机器并运行它。如果栈顶是一个“定义”对象,那么你就成功了。

现在你应该点头但不相信我,因为你正在考虑 PDF 流,它看起来像这样:

<< [/key value]* >> stream ...raw data... endstream endobj

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

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 处理其中的大部分。真正的挑战是能够遵守规范并确保您在字典中包含所有必需的值,并且它们在范围内并且语义正确。地狱,甚至是日期时间格式——这是非常明确的——是我库中的一大堆特殊情况代码,用于管理其他人在哪里搞砸了。

我可以(并且可能应该)写一本关于如何做到这一点的书。虽然很多边缘代码很脏,但整体结构可能非常漂亮。

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

于 2013-04-08T18:51:58.753 回答