PDF文件开发详解 第二章 PDF图像模型

1,188 阅读18分钟

PDF图像模型(PDF Imaging Model)

这一章里,我们会学习PDF图像模型,也就是可以在文档页面上执行的各种图形操作。包括用于描述这些操作的语言,还有与之相关的各种图形和图像的概念。

内容流(Content Streams)

一个PDF文件由一个或多个(固定大小的)页面组成,每个页面上的可见元素来自页面内容(page content),也就是我们本章讨论的主要内容。

页面内容使用一种特殊的基于文本的语法(和第 1 章中的PDF文件语法相关但不同)来描述,存储在内容流中,内容流是一种特殊的流对象。这种内容语法(content syntax)源自Adobe的Postscript语言,由一系列运算符及其操作数组成,每个操作数都是标准PDF对象(请参阅PDF对象)。示例 2-1 是绘制一个填充为蓝色的矩形的示例。

示例 2-1 绘制一个简单的矩形
0 0 1 rg               % set the color to blue in RGB
0 0 100 100 re         % draw a rectangle 100x100 with the bottom left at 0,0
F                      % fill it

首先我们要注意的是该示例是由后缀表示法(Reverse Polish Notation)表示的,运算符跟在操作数(如果有)后面。第二点是页面语法语言和Postscript不同,它不是真正的编程语言,因为它没有变量、循环或条件分支。

内容语法中的操作符可以在逻辑上分为三类。最重要的是绘制操作符(例如:re),绘制操作符会将内容实际绘制到页面上。其次是设置图形状态属性(例如颜色或笔宽)的操作符(例如:rg),因为如果不能设置绘图所需的图形状态属性,那绘图操作符就也没法起作用。最后还有一组操作符被称为标记内容运算符(marked content operators),可以操作某些特殊属性。

图形状态(Graphic State)

绘图操作依赖某些必要属性,例如颜色或笔宽。所以我们将从图形状态和相关操作符开始了解。图形状态可以被看作一个有很多属性(和setter方法)的类或结构体,不过没有getter方法。这意味着我们不能在给图形状态的某个属性赋值之后再通过一步操作直接让其变回原来的值。不过这并不意味着我们需要把属性相近似的图形一起绘制,或者进行大量“重置”操作。PDF处理器会维护图形状态的堆栈(就是传统编程意义上的堆栈),内容流可以将新状态推入或弹出已完成状态。我们可以保存状态,执行某些操作,然后返回到之前的状态。执行此操作的运算符是 q 和 Q(参见示例 2-2)。

示例 2-2 画两个矩形

4 w                    % set the line width to 4, for all objects
q                      % push/save the state
1 0 0 RG               % set the stroking color to red
0 0 100 100 re         % draw a rectangle 100x100 with bottom left at 0,0
S                      % stroke with a 4-unit width
Q                      % pop the state
q                      % push/save the state
0 1 0 RG               % set the stroking color to green
100 100 100 100 re     % draw a rectangle 100x100 with bottom left at 100,100
S                      % stroke with a 4-unit width
Q                      % pop the state

在示例 2-2 中,使用了一个我们之前没有见过的操作符(w)来设置图形状态下笔的宽度。这是一个常见操作。

除了笔的宽度,还有很多别的属性可以在使用线条(或任何形状的描边)时设置,包括线条的虚线样式和线条之间连接或相交的时候的样式。示例 2-3 是绘制两条虚线的示例。

示例 2-3 绘制虚线

     8 w
     q
          1 0 0 RG
          [5] 0 d       % simple 5 on, 5 off dash pattern
          100 100 m     % move to 100,100
          500 500 l     % draw a line to 500,500
          S
     Q
     q
          0 1 0 RG
          [5 10] 0 d    % 5 on, 10 of, dash pattern
          500 100 m     % move to 500,100
          100 500 l     % draw a line to 100,500
          S
     Q

示例 2-4 显示线条相交和覆盖时不同的样式(线的末端)。

示例 2-4 不同的线条相交和覆盖时的样式

          q
          0 1 0 RG
          10 w
          1 j                    % set the line join to round
          1 J                    % set the line cap to round
          100 500 m
          150 500 l
          150 450 l
          200 450 l
          200 400 l
          250 400 l
          250 350 l
          300 350 l
          300 300 l
          350 300 l
          350 250 l
          400 250 l
          400 200 l
          450 200 l
          450 150 l
          500 150 l
          500 100 l
          S
     Q
     q
          0 1 1 RG
          15 w
          2 j                    % set the line join to bevel
          50 500 m
          100 550 l
          500 550 l
          550 500 l
          550 100 l
          550 100 l
          500 50 l
          100 50 l
          50 100 l
          h                      % makes sure that the shape is a closed shape
          S
     Q

画家模型(The Painter’s Model)

参考上面的示例,绘制形状(或者我们叫它在PDF中的术语——路径,paths)的方式是首先定义或构建路径,然后要么描边(S)、填充(F/f)或即既描边也填充(B)该路径。每条路径都是按照它在内容流中出现的顺序绘制的,采用的是“先进先出”(FIFO) 操作的形式。也就是说如果我们想覆盖式地绘制路径,只需要在一条路径的绘制命令后面绘制就可以。符合这两种特点,通常被称为画家模型。

画家模型也被 SVG 和 苹果(Apple) 的 Quartz 2D 等图形系统使用。

示例 2-5 说明了这是如何工作的。

示例 2-5 三个重叠的矩形

1 0 0 rg
209 426 -114 124 re
F
0 1 0 rg
237 401 -114 124 re
F
F
0 0 1 rg
272 373 -114 124 re
F

开闭路径(Open versus Closed Paths)

画家模型中的的形状/路径可以是打开的关闭的,这决定了各种描边和填充操作在到达路径末端时如何完成。示例 2-6 中的路径,首先移动到 A 点,然后是一条线到 B 点,最后一条线到 C 点。

示例 2-6 一条简单的路径

100 100 m
100 200 l
200 200 l

如果现在使用在的之前的示例中看到的 S 运算符,则会绘制一条 L 形线,因为它只是按照定义的路径描边。但是如果使用 s 运算符,我们会得到一个三角形,因为路径会先被定义为关闭的(从最后一个点连接回第一个点)然后描再边。也可以使用 h 运算符显式关闭路径,然后使用 S 对其进行描边—— s 可被看成h 和 S 运算符的组合。

裁剪(Clipping)

剪裁使用路径(或文本)将绘图区域从整页限制到该页上的任意一个区域。一般用于仅显示对象的一小部分(通常但不一定限于光栅图形,raster graphics)。

使用W运算符将路径标记为剪切路径。然后可以对该路径进行描边(S)或填充(F/f),或者使用n运算符不进行绘制只进行裁剪操作。

示例 2-7 被圆形裁剪的矩形

q
    27.738 78.358 m
    27.738 56.318 45.604 38.452 67.643 38.452 c
    67.643 38.452 l
    89.681 38.452 107.548 56.318 107.548 78.358 c
    107.548 78.358 l
    107.548 100.396 89.681 118.262 67.643 118.262 c
    67.643 118.262 l
    45.604 118.262 27.738 100.396 27.738 78.358 c
    W n                % clip with no actual drawing
    1 0 0 rg
    1 0 0 rg
    0 0 0 RG
    97.5 48.5 -98 97 re
    B
    0 0 1 rg
    146.5 -0.5 -98 97 re
    B
Q

绘制路径(Drawing Paths)

虽然上面示例中展示的三个路径构造运算符(m、l 和 re)已经可以进行绘图了。但如果想绘制曲线,那我们需要更多的工具。

c运算符可以用来绘制贝塞尔(Bézier) 曲线(图 2-1),特别是三阶贝塞尔曲线。这种曲线是由当前位置点到目标点定义的,使用另外两个点(称为控制点)来定义曲线的形状。总共需要六个操作数。

虽然贝塞尔曲线非常灵活并且可以绘制非常复杂的图,但它不能用于绘制完美的圆。使用贝塞尔曲线能得到的最接近圆的方法是同四条贝塞尔曲线结合产生的,这些曲线的起始点和目标点都在圆上,并使用距离端点约0.6个单位的控制点来定义形状。

示例 2-8 绘制了一个圆,并演示了如何对路径进行描边和填充的时候使用不同的颜色。

示例 2-8 一个虚线圆圈

1 0 0 rg
0 0 0.502 RG
2 w
[5 2 ] 0 d
350 200 m
350 227.6 327.6 250 300 250 c
272.4 250 250 227.6 250 200 c
250 172.4 272.4 150 300 150 c
327.6 150 350 172.4 350 200 c
B

转换(Transformations)

如第一章所述,每个页面(请参阅页面)定义了一个区域(以用户为单位)。一般来说页面的原点 (0,0) 位于页面的左下方,y值随着页面向上增加而x值向右增加。这与标准笛卡尔坐标系的右上角部分一致。但是对于某些类型的绘图操作,我们可能希望以某种方式调整(或变换,这是正确的术语)坐标——反转/翻转、旋转、缩放等(见图 2-2)。

我们可以通过改变当前变换矩阵(CTM)来做到这一点。可以使用cm运算符来实现转换,cm运算符采用代表标准 3x2 矩阵的六个操作数。下图显示了最常见的转换类型以及用于它们的操作数。

TransformationOperand
移动(Translation)1 0 0 1 tx ty
缩放(Scaling)sx 0 0 sy 0 0”
旋转(Rotation)cosQ sinQ -sinQ cosQ 0 0
偏斜(Skew)1 tanA tabB 1 0 0

图 2-2 四种类型的转换

示例 2-9 给出了一些常见转换的示例。

示例 2-9 转换

q
     1 0 0 rg
     .50 0 0 .5 0 0 cm  % scale the drawing 50%
     0 0 100 100 re     % draw a 100x100 rect at the origin
     F
Q
q
     0 1 0 rg
     1 0 0 1 100 100 cm % move the origin to 100,100
     0 0 100 100 re     % draw a 100x100 rect at the origin
     F
Q

有时候我们需要做多次转换,这种操作称为连接矩阵。需要串联的最常见操作是旋转(参见示例 2-10)。这种操作比其他类型的变换更复杂,因为它涉及三角函数的使用。一般来说,串联使用旋转的原因是旋转总是围绕对象的左下角进行,而不是大多数人所期望的中心。因此,为了处理围绕中心点(或任意点)的旋转,我们需要先进行平移,然后进行旋转,然后再移回来。

示例 2-10 旋转的矩形

q
     0 1 0 rg
     1 0 0 1 50 50 cm                     % move origin to 50,50 (center point for rect)
     0.7071 0.7071 -0.7071 0.7071 0 0 cm  % 45 deg rotation
     1 0 0 1 -50 -50 cm                   % move the origin back
     0 0 100 100 re                       % draw a 100x100 rect at the origin
     F
Q

基础颜色(Basic Color)

到目前为止的示例中,我们一直使用基于RGB的颜色系统(在PDF术语中称为DeviceRGB)。这应该是最常用的颜色类型,计算机显示器用的也是这种颜色系统。在之前的示例中,我们给每个单独的颜色分量值使用了1或0。和其他RGB颜色系统(例如Windows GDI或HTML)不同,在PDF中,每个颜色分量的值是0到1之间的实数,而不是0到255之间的整数值。

数学上进行转换很简单:pdfValue = (255 – gdiValue)/255。

PDF同时支持十种其他的颜色空间(或者叫颜色模型),可以支持指定颜色空间。本节介绍另外两个设备颜色空间:DeviceGray 和 DeviceCMYK。如果有需要了解有关其他八种颜色空间以及如何使用图案或阴影来描边、填充对象的更多信息,请参阅ISO 32000-1:2008。

对于仅使用黑色和白色(或灰色)阴影的颜色,我们可以使用简单的DeviceGray颜色空间。运算符是 g(用于填充)和 G(用于描边),都只采用从 0(黑色)到 1(白色)的单个操作数。使用黑色的时候,最好尽可能使用DeviceGray而不是RGB,因为它可以产生更高质量的打印操作,同时节省PDF的文件大小(每次操作 3 字节与每次操作 8 字节)。

虽然屏幕使用RGB来定义颜色,但打印机使用称为CMYK的不同颜色模型,用于打印机中存在的青色、品红色、黄色和黑色墨盒。要在DeviceCMYK中描述颜色,可以使用k或K运算符以及每个颜色分量的四个操作数。

示例 2-11 说明了三种颜色空间的使用。

示例 2-11 三种基本色彩空间

10 w
q
    .5 G        % 50% gray, in Gray
    10 300 m
    100 300 l
    S
Q
q
    1 0 0 RG    % red, in RGB
    10 200 m
    100 200 l
    S
Q
q
    1 0 0 0 K   % cyan, in CMYK
    10 100 m
    100 100 l
    S
Q

标记内容操作符(Marked Content Operators)

标记内容操作符共有五个,用于标记一段内容,可以分为两类。MP和DP操作符用于标记内容流中的单个点。BMC、BDC和EMC操作符用尖括号来标记内容流中的一系列内容元素。

标记必须标记完整的内容元素,而不能是随便一个字符串。另外标记的内容必须只能包括在一个内容流里。

可能是为了方便PDF查看器定位,在标记内容中的单个点的时候,MP/DP运算符会与Name类型的操作数结合使用,这个名称类型的操作数被称为“标签”。MP和DP之间的区别在于DP运算符还用了第二个操作数——属性列表(请参阅属性列表)。示例 2-12 展示了一些示例。

示例 2-12 标记点的示例

% a content stream somewhere in a PDF
% we are using ABCD_ as an arbitrary second class name
/ABCD_MyLine    MP
q
    10 10 m
    20 20 l
    S
Q

/ABCD_MyLineWithProps << /ABCD_Prop (Red Line) >> DP
q
    1 0 0 RG
    10 200 m
    100 200 l
    S
Q

BMC和BDC运算符也和示例一样分别采用单个标签操作数或标签加上属性列表的方法标记内容。不过BMC和BDC需要用EMC操作符来定义结尾。和在示例 2-13中看到的一样,这些运算符比简单的点版本更有用。

示例 2-13 标记内容示例

% a content stream somewhere in a PDF
% we are using ABCD_ as an arbitrary second class name
/ABCD_MyLine    BMC
    q
        10 10 m
        20 20l
        S
    Q
EMC
/ABCD_MyLineWithProps << /ABCD_Prop (Red Line) >> BDC
    q
        1 0 0 RG
        10 200 m
        100 200 l
        S
    Q
EMC

属性列表(Property Lists)

使用DP和BDC标记内容的时候,还会带一个和内容相关的字典。这个字典叫属性列表,可能会包含对于被标记的内容特定的信息(例如可替换的内容)或者是对创建被标记内容的作者(或PDF的自定义处理器)有意义的信息。

简单的属性列表,其中所有键的所有值都是直接对象,可以作为直接对象写入内容流中(如前面的示例所示)。不过如果有任何键值对间接引用了内容流之外的对象的话,就需要将属性列表字典定义为当前资源字典的Properties键中的资源,然后按名称进行引用。

资源(Resources)

如果内容只由简单色彩空间中的路径组成,那这样的内容流是完全独立的,不需要引用其他的外部事物。但实际上PDF文件还是需要其他一些外部的内容,例如位图、光栅图像、文本。这些外部引用通过资源(Resources)字典进行管理,资源字典是页面(Page)字典中Resources键的值(参见示例 2-14)。资源字典中的每个键都有一个预定义好的名称,具体取决于列出的资源类型。每个键的值都是字典,包含资源的唯一名称以及对资源的间接引用。通常的做法是使用(短)可标识的名称/前缀(例如GS表示图形状态,IM表示图像等),然后逐渐递增。不过也可以随意起名字,张三李四都行。

示例 2-14 资源字典的示例

% in the page dictionary
/Resources <<
    /Font <<
        /F1 10 0 obj
        /F2 11 0 obj
    >>
    /XObject <<
        /Im1 12 0 obj
    >>
>>

先不用细究资源字典,后面会举例说明。

额外的图形状态(External Graphic State)

目前展示的示例中,所有图形状态属性都直接应用于内容流中。这是因为它们都只是简单的属性而且我们只用一次。不过实际中我们可能需要“预定义样式”这样的样式,然后直接用名字就能使用它。和其他格式(例如 HTML 或 DOCX)的样式表一样,这允许在不影响内容(流)的情况下轻松更新样式,同时还可以减小文件大小并提高性能。在PDF中,这些样式是使用称为图形状态参数字典或ExtGState(外部图形状态的缩写,是因为它们在内容流外面所以这么叫)调用的。

将ExtGState字典添加到资源字典中,然后就可以在内容流中使用gs运算符来调用预先定义的样式。示例 2-15定义了如何定义并使用大宽度自定义虚线图案的图形状态:

示例 2-15 使用 ExtGState

% in the page dictionary
/Resources <<
    /ExtGState <<
        /GS0 <<
            /Type /ExtGState
            /LW 10                   % 10 wide
            /LC 1                    % rounded caps
            /D [[2 4 6 4 2] 2]       % dash pattern
        >>
    >>
>>

% in the content stream
/GS0 gs
0 1 0 0 K
100 100 m
100 400 l
S

此外还有一些属性不能内联定义,只能使用ExtGState来定义。

透明度(Basic Transparency)

PDF图形模型在透明度领域支持很丰富的功能,可以在ISO 32000-1:2008第11条里详细探究这些功能。本章我们会学习一些基本的透明度的知识,可能和其他成像模型中很类似(例如 GDI+)。

最简单的透明度是用级别或百分比(从 0 到 1 的数字)代表对象的透明(或不透明)程度。透明度值为0的对象完全不可见,而值为1(默认值)的对象完全不透明。两者之间的值都代表一种透明度。与其他图形状态属性一样,我们可以在ExtGState字典中分别使用CA和ca键分别设置笔触和填充透明度值。

透明度只能作为ExtGState字典的一部分而不是直接在内容流中处理的原因是为了在将其引入PDF(1.4 版)时提供与老版本的兼容性。示例 2-16 给出了一些透明度示例。

示例 2-16 三个透明矩形

% in the page dictionary
/Resources <<
    /ExtGState <<
        /GS0 <<
            /CA 1
            /ca 1
        >>
        /GS1 <<
            /CA .5
            /ca .5
        >>
        /GS2 <<
            /CA .75
            /ca .75
        >>
    >>
>>

% in the content stream
q
    /GS0 gs    % no transparency
    1 0 0 rg
    209 426 -114 124 re
    f
Q
q
    /GS1 gs    % .5 transparency
    0 1 0 rg
    237 401 -114 124 re
    f
Q
q
    /GS2 gs    % .75 transparency
    0 0 1 rg
    272 373 -114 124 re
    f
Q

接下来的内容(What’s Next)

本章中,我们学习了内容流相关的内容还有如何引用外部资源。不过到目前为止,我们只介绍了如何绘制矢量图形(路径),PDF还支持很多其他类型的内容——比如文本和图像。

下一步,我们会学习图像相关的知识,光栅图像和处理矢量图形的方法。