PDF标准详解(六)——线与线型

PDF标准详解(六)——线与线型

Masimaro
2026-06-21 / 0 评论 / 2 阅读 / 正在检测是否收录...

首先距离上一篇PDF标准详解已经过去了一年多的时间了,之前断更主要是因为我当初迷上了配置Emacs,想学习Emacs的相关内容。最近Emacs的内容更新完了,现在我想有始有终,继续更新关于pdf的文章。很遗憾我现在已经不干与pdf相关的工作,我被分配到去搞流式文档的排版去了。已经很长时间没有研究pdf标准相关的内容了,关于pdf的一些详细细节我可能也不太了解,只能写一些我当初研究阅读过的一个大概。

前言

言归正传,我们继续pdf标准之旅;在前面的系列中,我们大概了解pdf由页面组成,而页面由各种绘图状态来控制。我们可以使用绘图状态在各个坐标位置绘制各种图形并进入图形控制状态,图形绘制完成之后通过一些操作符回到页面状态。pdf的渲染是一个不断进入各种状态,并且回退到页面状态。很像一个状态机或者更通俗的说像一个函数调用,在函数内部进行各种操作,函数退出之后堆栈恢复到主函数的状态。

我认为理解上一篇中给出的图形十分重要,它展示了pdf的分析框架。后面我们的内容就是针对整个框架完善其中的每一个边边角角。本节我们进入PathObj 路径对象的一个子部分,关于直线线条路径的描述

我觉PathObj 是很形象的一个描述,我们想象一下在野外爬山或者旅游,我们会形成一个行走的路线,每个街角怎么走,怎么停。最终我们有可能会在一些地图APP上形成一个路径图。PathObj也是类似的,我们规定了从哪个坐标开始,到哪个坐标走直线,到哪个坐标走曲线。最终pdf解析器和渲染程序会根据我们指定的路径绘制一个图形。

直线

在上一章,我们说了如何画一条直线,总体上就是先定义一个起点,然后通过 m(moveTo) 将画笔移动到这个起点,然后给出下一个点的坐标,并且跟上 l (LineTo),最后用 s,显示出来,例如下面的一个例子

3 0 obj     % 页面内容流
<< >>
stream      % 流的开始
400 400 m 100 100 l 800 400 l s
endstream   % 流结束
endobj

本篇我们将以这个例子为基础,介绍关于线条的基本属性

线宽

线宽的操作为m,它的定义是“从路径的垂直距离,在用户空间,是小于或等于一半行宽的所有点”。这句话听起来很拗口,但是实际上你可以想象这样一个场景,我们在使用钢笔或者圆珠笔写字画画时,假设笔尖出墨是往两边均匀的出墨,那么笔在纸上划过时笔尖到笔迹某一个点的垂直距离就是线宽

我们可以创造这么一个pdf来说明这个问题

4 0 obj 
<< >>
stream      % 流的开始
0.5 0.5 0.5 RG
20 w
100 100 m 200 200 l 400 100 l S

1 w
0 1 0 RG
100 100 m 200 200 l 400 100 l S

endstream   % 流结束
endobj

它的效果如下:

我们可以将绿色部分当作笔真实走过的轨迹,而灰色部分则是笔的墨水往两边扩散的结果。这里定义的20线宽是线的边缘到绿色部分的垂直距离

注意这里线宽定义的是实际线条宽度的一半。它的单位是一个非负的数字,如果我们采用设置线宽为0,那么此时的真实宽度是一个像素点。

线帽

它的英文是 line cap,它用来控制一条线在起点和终点(也就是没有和其他线条相交的开放端点)的边缘形状。它使用J来设置,它主要有三种样式

  • 0 J : 平头线帽,线条在端点处直接平切,不多出任何部分
  • 1 J : 圆头线帽,以线宽为半径,以起点或者终点为圆心画一个半圆
  • 2 J : 方头线帽,在起点或者终点位置多延伸出线宽一半的长度形成一个正方形的结尾。

下面是我将上述的pdf线条改成了圆头线帽的样式,具体的其他类型读者可以自行修改观察

4 0 obj 
<< >>
stream      % 流的开始
0.5 0.5 0.5 RG
1 J
20 w
100 100 m 200 200 l 400 100 l S

0 J
1 w
0 1 0 RG
100 100 m 200 200 l 400 100 l S

endstream   % 流结束
endobj

它的效果如下:

线连接样式

Line Join Style,线的连接样式是线相交或者接头处的样式,与上面的线帽类似,它使用j来作为操作符,它也是有三种样式

  • 0 j : 尖角连接,两条边的外侧直接延伸并相交,形成一个尖锐的角。这也是 PDF 的默认样式。
  • 1 j : 圆角连接,在转角处以一个圆弧进行平滑过渡,看起来比较柔和。
  • 2 j : 平角连接,把尖角直接“切掉”,在转角处形成一个平整的切面。

下面我们看一个平角连接的例子,其余样式如果读者有兴趣可以自行修改查看

4 0 obj 
<< >>
stream      % 流的开始
0.5 0.5 0.5 RG %设置RGB颜色
1 J
2 j
20 w
100 100 m 200 200 l 400 100 l S

0 J
1 w
0 1 0 RG
100 100 m 200 200 l 400 100 l S

endstream   % 流结束
endobj

它的效果如下:

虚线

虚线的操作符是 d 。我们可以简单的对上面的例子做一个改造,将实现改为虚线

stream      % 流的开始
0.5 0.5 0.5 RG
1 J
2 j
20 w
[6 4] 0 d
100 100 m 200 200 l 400 100 l S

0 J
1 w
[6 4] 0 d
0 1 0 RG
100 100 m 200 200 l 400 100 l S
endstream   % 流结束

它的效果如下:

从上面看,pdf的操作逻辑跟我们日常在纸张上写字画画一样,我们先选画笔的颜色、画笔的类型(这里是虚线型的画笔)、最后按照指定的轨迹(path) 来画图。

默认的画笔是直线类型的。虚线的语法结构为:

[线段长度 空白长度 ...] 相位 d

对于上面的例子,我们需要解释两个问题:

  1. 首先这个语法结构中的线段长度、空白长度以及相位都是什么意思
  2. 为什么都是例子中都是虚线但是实际显示时为什么只有一条虚线

我们先来解释第一个问题:

我们假设在一个一维的线段中有一个原始坐标为0,坐标单位为pdf单位的向右无限延长的坐标系(往哪个方向延长实际上取决于我们的path)。每个坐标单位是一段的连续的区域,例如1 表示从原点到1单位的这个区间, 3表示从2到3单位的这个区间

再想象一下,有一个存储了哪些区域有空白,哪些区域有线的列表。列表中存在数字的区域就有线。否则就没有线

根据这个描述,我们可以推导出 [3, 5] d 最终得到的数字列表为: [0, 1, 2, 8, 9, 10, 16, 17, 18, ...]

而相位则表示,从哪个区域块开始,如果是1相位,那么我们的列表就要在上述基础上舍弃一个元素,最终得到[1, 2, 8, 9, 10, 16, 17, 18, ...]

如果我们按照[3,5] 这个数组来操作虚线,在不同相位下得到的结果如下:

了解了这个问题,我们再来看上述的第二个问题?为什么上面的例子我们只看到了一条虚线。

因为上面我们开启了线帽,线帽会往左右两边进行延长,各延长一半的线宽。对于上面的例子,如果线宽是20,那么左右两边各延长10,但是我们给的空白区间只有4,所以延长的线帽部分会覆盖空白区域导致看起来是一条实线。而线宽为1的那条线左右两边只延长0.5,所以仍然有空白区间显示成虚线。各位读者可以尝试去掉线帽,再次查看效果。

利用上面的知识,我们再来分析一个相对复杂的虚线:

stream      % 流的开始
0.5 0.5 0.5 RG
20 w
[1 2 5 7 8 1 4 5 12 4 5] 0 d
100 100 m 400 100 l S
endstream   % 流结束

首先我们注意到,它里面的数字个数是奇数,无法凑成两两一对,对于这种情况,pdf会在后面将前面的所有数字再重新循环一遍也就是最终变成了[1 2 5 7 8 1 4 5 12 4 5 1 2 5 7 8 1 4 5 12 4 5]

我们可以每两个一组计算哪些区域有内容、哪些区域是空白的,最后将所有结果组合起来。

对于第一组: 第一个区域有内容,第二个、第三个是空白
对于第二组: 第4个区域到第8个区域有内容,而从第9个到第15个区域是空白
再往后就是后面的8个区域有内容、而1个区域是空白
后面的就依次类推。最终形成了一个类似条形码的东西。

各位读者可以自行查看效果,不过不同的pdf阅读器可能看到的效果不太一样,主要的区别在于阅读器是否会进行上述将数组中的数组重复一遍再解析。

总结

本文到此就结束了,本次介绍了一些关于线型的定义,包括线帽、线宽、线连接样式和虚线的定义。他们分别使用: Jwjd 来定义。

0

评论 (0)

取消