【问题标题】:Boost spirit can handle Postscript/PDF like languages?Boost Spirit 可以处理 Postscript/PDF 之类的语言吗?
【发布时间】:2013-04-08 17:49:02
【问题描述】:

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

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

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

【问题讨论】:

  • 我有一级扫描仪的 C 代码(没有 >)here 和后记 here 的部分翻译。
  • 找到了另一个(在后记中)here

标签: parsing pdf boost-spirit postscript lexer


【解决方案1】:

尽管这与 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) 和返回 IsTerminalIsTerminal,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);
}

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

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

所以当你这是一个 PDF 14 0 obj &lt;&lt; /Type /Annot /SubType /Square &gt;&gt; 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

同样,没有语法。 proc stream 查看堆栈的顶部,它应该是一个 PdfDict。如果是,它会消耗字符直到下一个换行符(扫描仪这样做),将当前文件位置存储在流中作为数据开始,从字典中读取流长度(这可能导致另一台机器更新),然后跳过超过流的末尾并将新的流对象推入堆栈。 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)。在写入引用对象时,您获取当前流指针并将其存储在外部参照中,然后写入&lt;objnum&gt; &lt;generation&gt; obj &lt;object contents&gt; 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。

【讨论】:

  • 这看起来真的很有趣,有两件事我不清楚:你的“逐行阅读+需要时跳转”的方法是如何管理xref 部分?写个pdf怎么样?当您需要逐行编写而不跳转时,您如何管理编写一个?
  • +1 很好的答案,完全同意。我喜欢精神,但不会在这里使用它(好吧,也许是为了用 Spirit Lex 进行词法分析)。 @user2244984 对于写作,你总是有(并且需要)一个内存表示来按输出流顺序遍历。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-10-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多