TrueType字体的字形由二次贝塞尔曲线构成。为什么在字形轮廓中会出现多个连续的非曲线点?

28

我正在编写一个TTF解析器。为了更好地理解TTF格式,我使用TTX提取了C:\Windows\calibri.ttf的“.notdef”字形数据,方法如下。

<TTGlyph name=".notdef" xMin="0" yMin="-397" xMax="978" yMax="1294">
      <contour>
        <pt x="978" y="1294" on="1"/>
        <pt x="978" y="0" on="1"/>
        <pt x="44" y="0" on="1"/>
        <pt x="44" y="1294" on="1"/>
      </contour>
      <contour>
        <pt x="891" y="81" on="1"/>
        <pt x="891" y="1213" on="1"/>
        <pt x="129" y="1213" on="1"/>
        <pt x="129" y="81" on="1"/>
      </contour>
      <contour>
        <pt x="767" y="855" on="1"/>
        <pt x="767" y="796" on="0"/>
        <pt x="732" y="704" on="0"/>
        <pt x="669" y="641" on="0"/>
        <pt x="583" y="605" on="0"/>
        <pt x="532" y="602" on="1"/>
        <pt x="527" y="450" on="1"/>
        many more points
     </contour>
     ...some other xml
</TTGlyph>

您可以看到连续的离散控制点。但是我了解到,TrueType字体由二次贝塞尔曲线组成,每个曲线有两个在曲线上的点(端点)和一个离散控制点。如何解释这些连续的离散控制点?


它们是贝塞尔曲线的非曲线控制点。在Wikipedia文章中,P1和P2是这些点。 - Hans Passant
5
嗨,汉斯。感谢您的回复。您提到的点P1和P2是用于三次贝塞尔曲线的。我知道n阶贝塞尔曲线有(n-1)个控制点。特别地,二次贝塞尔曲线只需要一个控制点。为什么由二次贝塞尔曲线制成的TTF字体文件有多个控制点呢? - lzl124631x
@HansPassant,请看上面。 - lzl124631x
1个回答

50

TTF解析需要应用http://www.microsoft.com/typography/otspec/glyf.htm以及来自Microsoft网站关于TTF格式的技术文档。这些告诉我们,曲线有两种类型的点:在曲线上的点和离曲线的点。在曲线上的点是“真实”的点,曲线通过这些点,而离曲线的点则是控制点,用于指导贝塞尔曲线的曲度。

现在,你所描述的“贝塞尔曲线”是正确的:单个(二次)贝塞尔曲线从1个真实点出发,由1个控制点引导,到达1个真实点(高阶曲线如三次、四次等在真实点之间有更多的控制点)。然而,由于二次曲线很难近似圆弧,因此它们通常不适合设计工作,但比高阶曲线更容易处理,因此对于使用TrueType进行字形轮廓的字体,我们只能使用它们。为了克服二次曲线的缺点,TrueType轮廓通常使用一系列贝塞尔曲线而不是单个曲线,以获得漂亮的均匀曲线,并且这些序列通常具有一个好的特性:在曲线上和离曲线的点以一种方式间隔排布,我们不需要记录序列中的每个点。

考虑下面的贝塞尔序列:

P1 - C1 - P2 - C2 - P3 - C3 - P4

如果我们添加了on信息,则在TTF中对其进行编码的方式如下:

P1 - C1 - P2 - C2 - P3 - C3 - P4
1  -  0 -  1 -  0 -  1 -  0 -  1

现在来介绍一个技巧:如果每个Pn都是一个曲线上的点,每个Cn都是一个控制点,并且 P2 恰好位于 C1 和 C2 的中间,P3 恰好位于 C2 和 C3 的中间,以此类推,那么这种曲线表示法可以被大大压缩,因为如果我们知道了 C1 和 C2,我们就可以知道 P2 等等。我们不需要明确列出任何中间点,我们只需要留给解析字形轮廓的工具去处理。

因此,TTF 允许您对具有上述属性的长贝塞尔序列进行编码:

P1 - C1 - C2 - C3 - P4
 1 -  0 -  0 -  0 -  1

正如您所看到的:我们在不失精度的情况下节省了大量空间。如果您查看TTX转储,您会看到每个点的on值反映了这一点。要获得P2、P3等内容,我们只需要这样做:

def getPoints(glyph):
  points = []
  previous_point = None;
  flags = glyph.flags

  for (i, point) in enumerate(glyph.point_array):
    (mask_for_point, mask_for_previous_point) = flags[i]

    # do we have an implied on-curve point?
    if (previous_point && mask_for_point == 0 && mask_for_previous_point == 0):
      missing_point = midpoint(point, previous_point)
      points.push(missing_point)

    # add the explicitly encoded point
    points.push(point)
    previous_point = point

  return points

运行此过程后,points 数组将具有交替的曲线点和非曲线点,并且贝塞尔曲线将被构建为:

for i in range(0, len(array), 2):
  curve(array[i], array[i+1], array[i+2]) 

编辑 经过一番搜索,http://chanae.walon.org/pub/ttf/ttf_glyphs.htm 在相当详细的讲解中涵盖了如何处理 glyf 表数据(ASCII 图形有点儿傻,但足够清晰)。

进一步编辑 经过几年的搜索,我成功地在苹果关于 TTF 的文档上找到实际解释的(或者至少是隐含的)内容,该文档位于 https://developer.apple.com/fonts/TrueType-Reference-Manual/RM01/Chap1.html#necessary,其中“图 13”表明:

尤其是曲线的转折点,即曲线的切线中点,不会增加任何额外信息,也可以被省略。

更进一步的编辑 ShreevatsaR 指出,在苹果文档的第二张图和第三张图之间的文本也与此相关:

通过去掉点 p2,也可以用一个更少的点来指定 FIGURE 2 中的曲线。点 p2 并不是严格需要的,因为它的存在可以被推断,并且其位置可以从其他点给出的数据重新构建。在重新编号剩余的点后,我们得到了 [FIGURE 3]。


1
哇!谢谢你的回答!我猜这个轮廓有点压缩,但不知道具体是怎么样的。你的回答似乎是正确的。我使用了你的方法,成功地解释了轮廓。你知道这个技巧在哪里有官方文档吗?我在TTF规范中没有找到(我认为规范不是很明确)。 - lzl124631x
2
非常感谢!与第13图标题具体相关的是您参考文献中第2和第3图之间的文本(https://developer.apple.com/fonts/TrueType-Reference-Manual/RM01/Chap1.html#points),似乎更为相关:“也可以通过删除点p2来指定在FIGURE 2中显示的曲线。点p2并不严格需要定义曲线,因为它的存在被暗示,并且其位置可以从其他点给出的数据重建。重新编号剩余的点后,我们得到[FIGURE 3]。” - ShreevatsaR
2
@zwcloud 图2中的p2是点p1和p3的中点。这就是你正在评论的答案的主要教训 :-) - ShreevatsaR
1
提示应用于轮廓,因此:之前。 - Mike 'Pomax' Kamermans
1
我不确定这个答案有多久了,但当我做自己的项目时,我遇到了它,并且它完美地回答了我的问题! - Michael Sohnen
显示剩余16条评论

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