PDF内容流中各种字形如何编码?

5
我正在开发一个程序,用于输出PDF文档。给定一系列UTF-8编码的字符和要用于呈现它的字体名称,我希望显示构成文档实际内容的相应字形。我希望能够显示像čö这样的国家字符。支持像aeffi这样的连字将是很好的。
问题是,我不知道要显示的实际字形如何指定(例如在内容流中)。
例如,如果我想显示字符串“Hello World”,我无需担心编码,只需写入(Hello World)Tj即可。然后,PDF阅读器将使用适当的字体呈现此字符串。
但是,如果我想展示字符串It is difficult to read the PDF specification all day. Prostě dočista nemožné!带有给定字体中的连字ffifiea以及捷克国家符号ěčé,我该怎么办?
我正试图理解PDF规范,但这并不容易。
  • 我如何找到对应于给定字符或连字的“字形代码”?
  • 这个代码如何在PDF内容流中被编码
非常感谢您的帮助。

编辑:我可能高估了这个问题。计算显示“普通欧洲文档”所需的字形数,我想不出超过256的方法。如果我的假设是正确的,我可以重新映射字体的编码。这应该足以涵盖拉丁字母、数字、标点符号、([等常见符号以及大量国家符号、连字和其他高质量排版元素。 (如果总字形数超过256,我可以实现优先级队列来选择最常用的连字。)

话虽如此,我认为我不需要使用CID键控字体。

即使如此,我仍然想知道如何将UTF-8编码的字符映射到任意字体的字形。我有可用的字体AFM。例如,对于DejaVu字体,字符信息如下:

C 63 ; WX 536 ; N question ; B 67 -15 488 743 ;
C 64 ; WX 1000 ; N at ; B 65 -174 930 705 ;
C 65 ; WX 722 ; N A ; B -6 0 732 730 ;

但是在映射了第256个字符之后,代码就变成了-1
C 255 ; WX 564 ; N ydieresis ; B -3 -223 563 767 ;
C -1 ; WX 722 ; N Amacron ; B -6 0 732 899 ;
C -1 ; WX 596 ; N amacron ; B 49 -15 568 746 ;

例如,如果我的输入序列是11100010 10000010 10101100(欧元符号),我应该如何知道它对应的字形名称,以便在/Encoding字典中进行映射?

1
PDF已经存在很久了,早在Unicode之前就开始使用。因此,它被“代码页病毒”感染。现在已经有许多现有的库可以读取PDF文件,提供了免疫保护,重新发明一个库的意义非常小。 - Hans Passant
无论如何,这是一个学校项目。谢谢您的回复! - David
从你的反应来看,今晚我可能睡不好觉。我还没有。这个程序的想法很简单:输入纯文本,输出 PDF。但是,要支持 UTF-8 输入。从你发来的维基百科文章部分链接来看,似乎我不能在单个文档中使用超过 256 个字符,但我知道这一定是可能的(中文符号有数千个并且被支持)。这是怎么工作的?我理解 PDF 内部的简单编码表是如何工作的,但是如何编码超过 256 个字符呢?再次感谢。 - David
1
顺便说一下,“难以阅读PDF规范”这个问题。我发现Adobe发布的文件(pdf)比ISO版本更易于阅读,无论是排版还是细微的专业语言。这可能只是因为我已经习惯了,但你可以试试看。 - user2846289
除了plinth的好答案之外,您可能还对我的答案感兴趣。请注意我在那里提到的“简单情况”-内容流中的双字节代码实际上是来自字体程序的字形ID。我认为这比手动发明Type1字体的编码要容易得多。Type0和其后代的所有结构可能看起来很可怕,但实际上并不是这样。 - user2846289
显示剩余2条评论
1个回答

14

字体编码取决于字体类型。通常有一个被定义为当前字体的字体资源,在该字体字典中有对基础字体的引用以及描述编码的方法(通过 /Encoding 键)。如果该键不存在,则编码将是“标准”的,但您可以使用其他简单的编码,如 /MacRoman/WinAnsi 作为编码值,或者指定一个标准编码和编码差异来显示差异。

如果你只处理8位字符,那么一切都很简单。对于许多早期的应用程序,它们会创建一些不同的字体,其中一个具有罗马编码,另一个将罗马字符映射到不可用字符。为了做到这一点,您的编码差异将包括对连字和其他通常未编码的符号的引用。这对于Type 1字体非常有效,但在TrueType字体的规范的部分中明确禁止使用:

无符号字体应该将其Encoding条目的值指定为MacRomanEncoding或WinAnsiEncoding,并且没有Differences数组

当您想要使用Unicode时,情况就大不相同了。在这种情况下,您将使用CID字体(基于字符ID的字体)。在这种情况下,字体引用的过程用于从您字符串中的字符编码映射到字体中的字符ID(反之亦然)。我强烈建议您阅读并充分理解PDF规范中第9.7节关于组合字体的内容,该节描述了您需要进行的所有操作,以便将UTF16BE编码到字符串中以使其在PDF中正确呈现。这是相当复杂的,因为有很多细节如果忽略将导致Acrobat中呈现的空白页面。

作为一名专业编写生成和消费PDF的代码的软件工程师,让我声明,当我被指派在我的代码中放入处理非规范兼容PDF的特殊情况时,我的内心都会死去一部分。请不要甚至想着发布任何未通过Preflight的文档。这与“Acrobat呈现它所以一定没问题”不同。让我举个例子 - 我见过许多在野外的文件,其中包含缺失FontDescriptor字典的关键元素,包括 /Ascent/Descent/CapHeight 等等。这些在Acrobat中呈现,但违反了规范,因为每个元素都是必需的。我知道Acrobat如何处理它 - 它附带一个巨大的字体度量数据库,如果找不到文件中的值,则会查找该值(甚至可能忽略文件中的度量)。我没有那个奢侈,因此我必须采取一些(潜在昂贵/无效)的临时措施。

你可能想考虑使用一个库来帮助你完成这项工作 - 也许是iText,因为它有一个足够好的教育许可证计划,我知道你是学生。还有一些基于C的库。也许你可以想出一种方法让GhostScript为你服务。

如果您不愿意或者无法遵循我的建议,关于坚持规范或使用明显遵守规范的库,请至少帮我填写文档信息字典中引用的尾部 / Creator 和/ Producer 字符串(参见14.3.3和7.5.5节)。这样,当我需要解析/消耗/操作您的文档时,我将直接怀疑你的家世。

让我们从上到下开始处理页面对象 - 我正在使用来自我的库的输出,并剥离掉我认为您不需要的内容:

1 0 obj << 
    /Type /Page 
    /Parent 18 0 R 
    /Resources << 
       /Font << 
          /U0 13 0 R 
          >>
       /ProcSet [ /PDF /Text ]
       >>   
    /MediaBox [ 0 0 612 792 ]
    /Contents 19 0 R    
    /Dur -1 
    >>
 endobj

U0是一个用于Unicode文本的字体参考。

内容流旨在打印以下文本:Greek: Γειά σου κόσμος

BT /U0 24 Tf 72 670 Td 
(\000G\000r\000e\000e\000k\000:\000 \003\223\003\265\003\271\003\254\000 \003\303\003\277\003\305\000 \003\272\003\314\003\303\003\274\003\277\003\302) 
Tj ET

参考的字体字典如下:

13 0 obj << 
    /BaseFont /DejaVuSansCondensed 
    /DescendantFonts [ 4 0 R  ]
    /ToUnicode 14 0 R 
    /Type /Font 
    /Subtype /Type0 
    /Encoding /Identity-H 
>>
endobj

该流具有包含以下PostScript代码的/ToUnicode入口点:

/CIDInit /ProcSet findresource begin 12 dict begin begincmap /CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def /CMapName /Adobe-Identity-UCS def /CMapType 2 def 1 begincodespacerange <0000> <FFFF> endcodespacerange 1 beginbfrange <0000> <FFFF> <0000> endbfrange endcmap CMapName currentdict /CMap defineresource pop end end

这是由CID字体规范定义的。

而DescendantFonts数组指向此对象:

4 0 obj << 
    /Subtype /CIDFontType2 
    /Type /Font 
    /BaseFont /DejaVuSansCondensed 
    /CIDSystemInfo 7 0 R 
    /FontDescriptor 8 0 R 
    /DW 1000 
    /W 9 0 R 
    /CIDToGIDMap 10 0 R 
>>

CIDToGIDMap是一个压缩流,包含实际映射。CIDSystemInfo是<</Registry (Adobe) /Ordering (USC) /Supplement 0>>(因为我在所有unicode字体中共享它)。FontDescriptor很简单,W数组从字体度量衍生而来。

通过这些细节,你明白为什么我不会轻易说“走开,别再污染我的环境了”了吗?

我真的开始质疑这个任务的性质。编写简单的PDF文件是一回事,但编写能够处理任意OpenType/TrueType字体中的完整Unicode的代码需要你理解CID规范和TrueType规范(提示:我有一个完整的TrueType解析器,可以提取字体中任何字形的所有度量信息,以便我输出/W数组)。

然而,如果你只需要输出到Type 1字体,那么你的生活就变得容易得多,因为你将获取整个UTF8流并将其作为Unicode读入,对于每个独特字符,你将使用此表格建立一个从Unicode字符到字形名称和内部字符编号的映射。内部字符编号本质上是所输入字符的唯一索引(mod)。例如,如果你在页面上有不到257个独特字符,那么你将只有一个字体,该字体的编码映射到以所输入顺序映射字符。如果你使用“abcba”作为输入,则PDF中的输出字符串将是(\000\001\002\001\000),并将映射到一个具有差异数组的编码字典,该数组为[0/a/b/c]。如果你有n个唯一字符,其中n>256,则你将拥有(n/256)+1个字体,并且每个字体都具有编码。

如果你的老师/教授想在短时间内得到除Type 1字体之外的任何东西,那么他对学生的期望不切实际和/或对输出质量的期望很低。你应该问自己是否需要处理CID字体,如果需要,那么你的教授至少是个虐待狂。对我这样的经验丰富的专业人士而言,提取宽度的TrueType解析器大约需要4天时间。我有两个优势:(1)使用托管语言(C#)减少了C语言中会出现的问题,并且还可以使用反射来自动解析;(2)当没有干扰时,我编写的代码比典型的学生快10-20倍,因此我的32小时将转化为320个学生小时,或者更少(然而,我的代码有不同的约束条件——它必须优雅地处理任何垃圾字体),如果你被允许偷一些像stb这样的东西,那么就称之为200个或更少。这仅仅是获取字体描述符中的一个特定元素。


非常感谢您的回复。令人惊讶的是有多少人要求我帮他们做各种事情,特别是不要自己创建任何PDF文档! :) 实际上,我真的希望这样做得正确。缺乏免费的PDF验证工具使它变得困难。我不能直接使用库,因为那是禁止的。我会像您建议的那样阅读Adobe PDF参考书的第9.7节。如果我通过了这个阶段,您介意我时不时地向您请教吗? - David
此时,我认为这已经超出了你的问题范围(在我写这篇文章时,我不知道这是一份作业)。我能提供的最好建议是,你应该查看简单的输出,我已将其编辑到我的答案中,并尝试复制它。 - plinth
我已经更新了我的问题。从您的角度来看,完全重新映射字体编码并在许多情况下破坏ASCII兼容性是否是一种不好的做法? - David
1
感谢您的详细解释。我相信每个文档256个字符就足够了。当我想到写这篇文章时,我的教授问我想支持哪些字体格式。我回答说:“这次我会坚持使用Type 1。”他微笑着点头。当我说如果有时间我会研究OpenType时,他又笑了。现在我知道为什么了! - David
2
Preflight是Adobe Acrobat中内置的一个工具(不是Reader,因此不免费),它可以对PDF文档运行多种不同类型的验证,包括语法和规范合规性。 - plinth
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接