PDF(Portable Document Format)是一种广泛使用的文件格式,用于以一种独立于设备和平台的方式呈现文档。尽管PDF设计为一种最终的、类似于纸上墨水的格式,但在某些情况下,比如无法访问源格式时,编辑PDF文件成为必要。本文将探讨PDF文档中的图形基础,以及如何在必要时编辑这些图形。
PDF文档包含多种类型的信息,例如元数据(作者、标题等)、表单字段、导航数据(如书签)、注释(如评论)以及最重要的,图形。图形大致可以分为三类:曲线、文本和图像。PDF页面上的图形由一系列操作符描述。操作符可以分为三组:
每个操作符可以带零个或多个操作数。以下是一个绘制直线的简单示例:
150 250 m % 设置当前点到 (150, 250)
150 350 l % 向 (150, 350) 绘制直线
1 0 0 RG % 设置描边颜色为红色
S % 描边线条
操作符位于其使用的操作数之后。在第一行中,操作符 'm' 使用操作数 150 和 250。
以下是一个涉及文本的示例:
/F1 24 Tf % 设置字体为 F1,字体大小为 24
100 100 Td % 将文本位置移动到 (100, 100)
(Hello World) Tj % 绘制文本 'Hello World'
在第一行中,操作符 Tf 接受操作数 /F1 和 24。操作数 /F1 是字体的名称。
以下是绘制图像的一行代码示例:
/I1 Do % 绘制图像
类似于选择字体,/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定义。让看看这在代码中是如何实现的:
C#
float width = 612; // Letter页面的宽度
float height = 792; // Letter页面的高度
float dpi = 600; // 输出设备是600 dpi位图
Bitmap bitmap = new Bitmap((int)(width * dpi / 72), (int)(height * dpi / 72));
PointF[] points = new PointF[] {
new PointF(0, 0), // 左下角
new PointF(0, height),
new PointF(width, height),
new PointF(width, 0)
};
Console.WriteLine(string.Join("; ", points.Select(p => string.Format("({0}, {1})", p.X, p.Y))));
Matrix ctm = new Matrix();
ctm.Scale(1, -1); // 垂直翻转轴
ctm.Translate(0, -bitmap.Height);
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点乘以200点的页面。以下图像显示了用户空间坐标系统叠加在空白页面上:
绘制一个50乘以50的红色正方形和一个25乘以25的较小蓝色正方形在红色正方形内部,如下所示:
接下来,通过平移(50, 75)来变换用户空间。注意,这是在绘制图形之前完成的。
最后,用户空间旋转30度,如下所示:
因此,不是变换正方形,而是变换用户空间,然后在该用户空间内绘制正方形。这可能会让人感到反直觉。
从开发的角度来看,一系列操作符并不是一个方便的格式。例如,不能轻松地导航到页面上的图像并检索其位置。它的属性取决于所有先前操作符的累积,所以首先需要处理它们。对于文本和曲线也是如此。
更改图形,如移动单个图像或旋转文本片段,会更加困难,因为必须插入操作符,以便它们只影响目标图形。
PDFKit.NET允许将页面上的所有图形提取为形状对象集合。在内部,它将执行解释操作符的艰苦工作,从绘制操作符创建形状对象,并分配反映当前图形状态的属性。提取形状后,可以删除形状,插入新形状并更改它们各自的属性。完成后,可以将形状写回到PDF页面。这将反过来生成所需的操作符和操作数序列。
为了演示使用形状编辑图形,将替换一个徽标。请参见下面原始PDF和替换徽标后的PDF图像:
C#
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);
// 添加修改后的形状到新文档
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)
{
// 递归
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);
}
}
}