PDF 图形基础及其编辑方法
本文将介绍 PDF 图形的基础知识以及在必须进行编辑时如何编辑图形。
我经常听到这样的问题:“如何更改 PDF 中的图形,例如用其他文本替换文本或用另一个徽标替换一个徽标?” 通常来说,这并不是一个好主意。PDF 的设计目的不是用于编辑;它被设计成最终格式,就像纸上的墨水一样。尽管如此,在某些情况下,例如当您无法访问源格式时,编辑 PDF 也是有要求的。本文将介绍 PDF 图形的基础知识以及在必须进行编辑时如何编辑图形。
PDF 图形
PDF 文档包含各种类型的信息,例如元数据(作者、标题等)、表单字段、导航数据(如书签)、注释(如评论),以及最重要的,图形。图形大致可分为三类:曲线、文本和图像。PDF 页面上的图形由一系列操作符描述。操作符可分为三组:
- 绘制操作符,用于绘制曲线、文本和图像
- 图形状态操作符,用于执行诸如选择字体、选择颜色或变换坐标系(稍后将详细介绍)之类的操作
- 标记内容操作符,用于将高级信息与图形关联,但不会影响外观。出于本文的目的,我将忽略这一组。
每个操作符接受零个或多个操作数。下面是一个绘制直线段的简单示例:
150 250 m % set the current point to (150, 250)
150 350 l % append a straight line to (150, 350)
1 0 0 RG % set the stroke color to red
S % stroke the line
操作符后面是操作符使用的操作数。在第一行,操作符 'm' 使用操作数 150 和 250。
这是一个涉及文本的示例:
/F1 24 Tf % set font to F1 and font size to 24
100 100 Td % move text position to (100, 100)
(Hello World) Tj % draw the text 'Hello World'
在第一行,操作符 Tf
接受操作数 /F1
和 24
。操作数 /F1
是字体的名称。无需详述,只需知道 /F1
可以解析为 PDF 文档内部或外部的实际字体即可。
最后,这是绘制图像的一行代码:
/I1 Do % draw image
与选择字体类似,/I1
会解析为 PDF 文档内部的实际图像。
图形状态
如前所述,操作符可分为绘制操作符和图形状态操作符。当操作符从上到下处理时,会维护一个图形状态。图形状态操作符会更改图形状态,而绘制操作符的结果会受到图形状态的影响。在第一个示例中,我们看到 RG
操作符将描边颜色更改为红色,而 S 操作符使用当前描边颜色绘制一条线。
其他图形状态操作符用于设置线宽、虚线样式、填充颜色、字体大小等。最后,有两个特殊操作符分别用于保存 (q
) 和恢复 (Q
) 图形状态。简单来说:恢复操作符会将图形状态更改回先前保存操作符时的状态。它们成对出现,可以嵌套。
坐标系
PDF 成像模型的一个关键部分是坐标系。坐标系决定了页面上的给定坐标(例如 (150, 250)
)的位置以及尺寸的范围。PDF 定义了不同的坐标系。最重要的两个是用户空间和设备空间。
设备空间
设备空间由输出设备决定,例如打印机或显示器,PDF 页面最终会在其上渲染。假设我们要将 PDF 页面渲染到 300 DPI 的 Windows 位图,那么从 Windows 开发的角度来看,设备空间的起点位于左上角,x 轴指向右方,y 轴指向下方,单位(像素)的长度是 1/300 英寸。
用户空间
与设备空间相反,用户空间是设备无关的。对于每一页,其初始设置的原点位于左下角,x 轴指向右方,y 轴指向上方,单位的长度是 1/72 英寸或 1 个点。上述 PDF 操作符示例中的坐标位于用户空间。
用户空间到设备空间的映射
用户空间中的坐标如何变换为设备空间中的坐标是由当前变换矩阵(CTM)定义的。我们来看看代码中如何实现这一点:
// width and height of a Letter page
float width = 612; // 612 points = 8.5 inches
float height = 792; // 792 points = 11 inches
// output device is a 600 dpi bitmap
float dpi = 600;
Bitmap bitmap = new Bitmap((int)(width * dpi / 72), (int)(height * dpi / 72));
// 4 page corners in user pace
PointF[] points = new PointF[] {
new PointF(0, 0), // bottom-left corner
new PointF(0, height), // top-left corner
new PointF(width, height), // top-right corner
new PointF(width, 0) // bottom-right corner
};
Console.WriteLine(
string.Join("; ", points.Select(p => string.Format("({0}, {1})", p.X, p.Y))));
// calculate the coordinates of the corners in device space
Matrix ctm = new Matrix();
// flip vertical axis
ctm.Scale(1, -1);
ctm.Translate(0, -bitmap.Height);
// resolution
ctm.Scale(dpi / 72f, dpi / 72);
ctm.TransformPoints(points);
Console.WriteLine(
string.Join("; ", points.Select(p => string.Format("({0}, {1})", (int)p.X, (int)p.Y))));
更改用户空间
CTM 是图形状态的一部分,可以使用 cm 操作符进行更改。cm 操作符接受六个操作数,代表一个变换矩阵。更改 CTM 会影响后续的绘制操作符,正如您将在以下示例中看到的。
我们有一个页面,尺寸为 200 pt x 200 pt。下图显示了空白页面以及叠加在其上的用户空间坐标系:
我们绘制一个 50x50 的红色正方形,并在红色正方形内绘制一个 25x25 的蓝色小正方形,如下所示:
接下来,我们将用户空间平移 (50, 75)。请注意,这是在绘制图形之前完成的:
最后,我们将用户空间旋转 30 度,如下所示:
所以,我们不是变换正方形,而是变换用户空间,然后在该用户空间内绘制正方形。根据您的背景,这可能感觉有些违反直觉。
Shapes
从开发角度来看,操作符序列并不是一个方便的格式。例如,您无法轻松导航到页面上的某个图像并检索其位置。其属性取决于所有先前操作符的累积,因此您必须先处理所有操作符。文本和曲线也是如此。
更改图形(例如移动单个图像或旋转一段文本)会更加困难,因为您必须插入操作符,使其仅影响目标图形。
PDFKit.NET 允许您将页面上的所有图形提取为一组形状对象。在内部,它将完成解释操作符、从绘制操作符创建形状对象以及分配反映当前图形状态的属性的所有繁重工作。提取形状后,您可以删除形状、插入新形状并更改其各自的属性。完成后,您可以将形状写回 PDF 页面。这将生成所需的操作符和操作数序列。
示例:替换徽标
为了演示形状在编辑图形中的用法,我们将替换一个徽标。请看下面原始 PDF 和替换徽标后 PDF 的图像:
这是所有代码:
static void Main(string[] args)
{
using (FileStream fileIn = new FileStream(
"indesign_shortcuts.pdf", FileMode.Open, FileAccess.Read))
{
Document pdfIn = new Document(fileIn);
Document pdfOut = new Document();
foreach (Page page in pdfIn.Pages)
{
ShapeCollection shapes = page.CreateShapes();
replaceLogo(shapes);
// add modified shapes to the new document
Page newPage = new Page(page.Width, page.Height);
newPage.Overlay.Add(shapes);
pdfOut.Pages.Add(newPage);
}
using (FileStream fileOut = new FileStream(
"out.pdf", FileMode.Create, FileAccess.Write))
{
pdfOut.Write(fileOut);
}
}
}
static void replaceLogo(ShapeCollection shapes)
{
for (int i = 0; i < shapes.Count; i++)
{
Shape shape = shapes[i];
if (shape is ShapeCollection)
{
// recurse
replaceLogo(shape as ShapeCollection);
}
else if (shape is ImageShape)
{
ImageShape oldLogo = shape as ImageShape;
shapes.RemoveAt(i);
ImageShape newLogo = new ImageShape("new-logo.png");
newLogo.Transform = oldLogo.Transform;
newLogo.Width = oldLogo.Width;
newLogo.Height = oldLogo.Height;
shapes.Insert(i, newLogo);
}
}
}