在 PDF 中添加图像和文本框
一个轻量级的 C# 库,可以动态地向 PDF 添加图像和“圆角矩形”,然后安全地将 PDF 嵌入网页
引言
我需要一种动态创建 PDF 文档的方法,特别是发票和类似的财务文档。
该软件还必须能够无缝集成到我正在开发的应用程序中。
显然,市面上有很多软件,从开源到相当昂贵的商业应用程序都有,但是,我想要一些我可以集成到商业应用程序中,许可费用最少或没有,并且在无法访问源代码的情况下没有支持问题的东西。
另一个标准是代码量最少。我没有看到有 3MB 的代码有什么意义,而我可能只需要其中的 5%。
经过长时间的网上搜索,我终于在 CodeProject 上发现了一篇文章(用于在 C# 中创建带有表格和文本的 PDF 的 PDF 库)。Zainu 的这篇优秀文章向我介绍了创建 PDF 的基本概念。
Zainu 的文章介绍了创建 PDF 结构并添加文本(无论是单行/句子还是以表格形式格式化)所需的基本概念。
在 Zainu 的许可下,我扩展了他的代码库,包括添加 JPG 图片,添加“圆角矩形”形式的文本框(如右侧图像所示),最后,将完成的 PDF 显示在网页中,而不是像通常那样使用 `` 标签链接到它。
背景
为了完全理解这个库背后的代码,您应该阅读 Zainu 的文章,因为我不会在这里重复讨论相同的主题。
部分原始代码已被修改,这些更改都已在代码本身中注释。
PDF 的工作原理
虽然我不会重新讨论原始文章的概念,但我会重新审视 PDF 的概念,以展示添加图像和“圆角矩形”所需的知识。
这是如果您下载并运行附加代码所生成的 PDF 标记的示例。
请记住,尽管您可以在文本编辑器中阅读标记,但 PDF 实际上是一个二进制文件,并且(除最简单的情况外)必须如此处理。
您可以通过下载Adobe PDF 手册来找到更详细的 PDF 解释。它只有大约 1300 页……
PDF 标记 | 含义 |
%PDF-1.5 %?? |
这是 PDF 版本 1.5 - 双问号只是为了让 FTP 和类似软件包知道在传输时这是一个二进制文件。 |
8 0 obj << /Type /Page/Parent 2 0 R /Rotate 0 /MediaBox [0 0 595 842]/CropBox [0 0 595 842] /Resources<</ProcSet[/PDF/Text] /Font<</T1 3 0 R/T2 4 0 R/T3 5 0 R/T4 6 0 R>> /XObject <</I1 10 0 R >>>> /Contents 9 0 R >> endobj |
'X 0 obj' 表示这是 PDF 中的一个对象 - X 是其唯一编号。 此对象 (8 0) 描述一页,设置页面大小,然后定义所需的资源,在本例中为 T1、T2、T3 和 T4 字体,它们分别在对象 3 0、4 0、5 0 和 6 0 中描述。 XObject 在本例中指的是一个名为 I1 的图像,描述它的数据可以在对象 10 0 中找到。 最后,“Contents”(本质上是告诉 PDF 显示什么的标记)可以在编号为 9 0 的对象中找到。 'Parent' 引用指向对象 2 0,该对象记录了整个文档的页数(在本例中只有 1 页)并显示哪个对象描述了每一页的内容。 |
9 0 obj<</Length 989 >>stream q 144 0 0 100 300 700 cm 1 0 0 1 0 0 cm /I1 Do Q BT/T3 12 Tf 105 699 Td (Round Rectangle Header) Tj ET endstream endobj |
此对象包含描述实际页面的标记,其含义如下: 将文本“XYZ”放置在 x,y 位置,使用 Z 字体 或 绘制图像 位于 x,y,尺寸为 w,h 等。在代码中,它是 `SetStream`。 |
1 0 obj<</Type /Catalog/Lang(EN-US)/Pages 2 0 R>> endobj |
文件根目录,表示文档的索引(或页面树)可以在对象 2 0 中找到。 |
2 0 obj<</Count 1/Kids [ 8 0 R ]>> endobj |
文档索引 - 此文档有一页(Kids),信息可以在对象 8 0 中找到。 |
3 0 obj<</Type/Font/Name /T1/BaseFont/Times-Roman /Subtype/Type1/Encoding /WinAnsiEncoding>> endobj 4 0 obj<</Type/Font/Name /T2/BaseFont/Times-Italic /Subtype/Type1/Encoding /WinAnsiEncoding>> endobj 5 0 obj<</Type/Font/Name /T3/BaseFont/Times-Bold /Subtype/Type1/Encoding /WinAnsiEncoding>> endobj 6 0 obj<</Type/Font/Name /T4/BaseFont/Courier /Subtype/Type1/Encoding /WinAnsiEncoding>> endobj |
描述文档中使用的字体。 |
10 0 obj <</Name /I1 /Type /XObject /Subtype /Image /Width 144 /Height 100 /Length 29779 /Filter /DCTDecode /ColorSpace /DeviceRGB /BitsPerComponent 8 >> stream [ 表示 jpg 图像的字节数据 ] endstream endobj |
描述一个图像。 请注意,实际的字节数据丢失了 - 您稍后会了解到如何添加它。 |
7 0 obj<</ModDate(D:20070501024237+10'00') /CreationDate(D:20070501024237+10'00') /Title(Title)/Creator(Your App Name) /Author(System Generated /Producer(www.My New App.com.au)/Company(My Company Name)>> endobj |
文档的属性,例如创建者和创建时间等。 |
xref 0 11 0000000000 65535 f 0000001275 00000 n 0000001332 00000 n 0000001374 00000 n 0000001473 00000 n 0000001573 00000 n 0000001671 00000 n 0000031745 00000 n 0000000014 00000 n 0000000234 00000 n 0000001766 00000 n |
文档中每个对象的字节偏移量。这在 Zainu 的原始文章中有解释。 |
trailer <</Size 11 /Root 1 0 R /Info 7 0 R /ID[<5181383ede94727bcb32ac27ded71c68> <5181383ede94727bcb32ac27ded71c68>] >> |
'Root' 指的是文档的起点,称为页面树(本例中为对象 1 0)。 |
startxref 31959 %%EOF |
文件结束。 |
使用代码
下载上面的 zip 文件并解压到合适的位置(或在 Visual Studio 中创建一个新的 Web 应用程序)。zip 文件包含六个文件:
- PDFLibrary.cs
- Default.aspx
- Default.aspx.cs
- streampdf.aspx
- streampdf.aspx.cs
- myimage.jpg
文件 *PDFLibrary.cs* 应放在 App_Code 文件夹中,其余文件放在应用程序的根目录。
将浏览器指向 *default.aspx* 文件,您应该会看到一个按钮。单击该按钮应会创建 PDF 并将其显示在网页中。
一切如何运作
让我们先看看“圆角矩形”。
它们基于“三次贝塞尔曲线”。如果您曾经使用过 PhotoShop 或类似的图形软件,您可能已经使用过这种方法而没有意识到。
本质上,我们要做的就是使用八个路径来形成一个区域,以创建“圆角矩形”。其中四个路径是基于贝塞尔曲线的半径,加上连接它们的四条直线。然后描边形成边框,并填充有界区域以形成背景。然后在贝塞尔区域顶部绘制一个简单的矩形以形成文本框。
如果您对完整细节感兴趣,请查看 Adobe PDF 手册,其中介绍了其背后的数学原理。对于我们其他人来说,我们只需要知道它有效即可!
现在让我们看看实际的代码。首先,我们创建一个新对象来表示代码中的矩形:
RoundRectangle rr = new RoundRectangle();
然后指定边框、主背景和文本框背景颜色的颜色。
ColorSpec rrBorder = new ColorSpec(0, 0, 0); //main border colour
ColorSpec rrMainBG = new ColorSpec(204, 204, 204); //background colour of the
//round rectangle
ColorSpec rrTBBG = new ColorSpec(255, 255, 255); //background colour of the
//rectangle on top of the
//round rectangle
最后,由于这只是 PDF 中的标记,我们将标记添加到 PDF 内容流中。
content.SetStream("q\r\n"); //initialise the PDF graphics cursor
content.SetStream(rr.DrawRoundRectangle(45, 582, 240, 130, 20, 0.55, 20, 90,
1, rrBorder, rrMainBG, rrTBBG)); //Draw the rectangle
content.SetStream("Q\r\n"); //close the graphics cursor in PDF
`DrawRoundRectangle` 方法有十二个参数:
LLX
LLY
rrWidth
rrHeight
CornerRadius
Circularity
HeaderHeight
TextBoxHeight
Border
BorderColor
MainBG
TextBoxBG
LLX 和 LLY 是框左下角的水平和垂直坐标,rrWidth 和 rrHeight 是框的宽度和高度(请记住,所有坐标都以 1/72 英寸而不是像素为单位)。
CornerRadius 参数如图 2 所示。HeaderHeight 参数是顶部可以放置文本的区域的垂直高度。它不能小于半径,否则顶部的文本区域矩形会重叠。
TextBoxHeight 是文本框的高度,并且会垂直居中。最后三个颜色参数是我们之前创建的三个 `ColorSpec` 值。
最后是 Circularity 参数。这用于更改框角部的实际形状。
由于我想让每个角落都镜像反射,我决定根据角落的半径和一个称为 Circularity 的常数来计算图 1 中显示的 (x2,y2) 和 (x3,y3) 值(这些值在 PDF 标记中用于描述曲线)。(x1,y1) 的值是 PDF 中当前的图形光标位置,(x4,y4) 的值是曲线的终点,也是新的图形光标位置。
值为 0 时,您会得到一条直线(实际上是八边形)。如果将值增加到 0.55,您将获得一个完美的半径。随着值增加到 1,角落会变得更紧/更小。一旦值开始超过 1,就会出现其他有趣的角落形状。
就这样。此代码假定最终文档只有一页,并且文本框中的文本行数固定。但是,将 `textAndtable.AddRow` 方法与 `DrawRoundRectangle` 方法结合起来,动态创建文本框的垂直尺寸并在需要时跨多页包装,这并不难。
绘制直线
在框内绘制直线(也许是为了指定列)可以使用 `textAndtable` 类,如果文本是表格形式的,或者您可以使用 `line.DrawLine` 方法。该方法仅接受直线的起点(xs,ys)、终点(xe,ye)坐标以及线宽和颜色,并将标记添加到 PDF 内容流中。这对于分隔单个文本元素很有用。
向 PDF 文档添加图像
在 PDF 页面上显示图像比创建“圆角矩形”要复杂得多。由于图像无法用数学方式指定,我们需要向 PDF 提供更多信息,然后才能渲染它。
将图像添加到 PDF 有三个部分。这些在本文开头描述简单 PDF 标记的表格中显示。
- 创建索引
- 创建描述图像的参数和字节数据
- 将图像绘制到文档
让我们更详细地看看这涉及到什么。
PDF 标记 | 在添加图像方面的含义 |
8 0 obj << /Type /Page/Parent 2 0 R /Rotate 0 /MediaBox [0 0 595 842]/CropBox [0 0 595 842] /Resources<</ProcSet [/PDF/Text] /Font<</T1 3 0 R/T2 4 0 R/T3 5 0 R/T4 6 0 R>> /XObject <</I1 10 0 R >> >> /Contents 9 0 R >> endobj |
首先,我们需要告诉 PDF 在哪里可以找到描述图像的数据。 这是 `CreateImageDict` 方法。在这种情况下,我们告诉 PDF 描述名为“I1”的图像的数据可以在编号为 10 0 的对象中找到。 |
9 0 obj<</Length 989 >>stream
BT/T3 12 Tf 105 699 Td (Round Rectangle Header) Tj ET endstream endobj |
现在我们需要向 PDF 提供一些关于在页面上何处放置图像的信息。q 和 Q 表示我们正在使用图形光标(有关语法的完整详细信息,请参阅 PDF 手册)。 接下来的三行描述了图像相对于页面的位置、页面宽度和高度。 如果您查阅 PDF 手册,还可以对图像应用各种变换,例如旋转、缩放、倾斜以及许多其他更高级的功能。如果您想使用它们,这些就需要添加到代码中。 此标记通过 `AddImageResource` 方法作为 `GetPageDict` 方法的一部分添加到内容流中。 |
10 0 obj |
最后,我们需要描述实际的图像。 PDF 需要诸如名称、像素尺寸、数据压缩类型(jpg、gif、png、tif 等都有不同的压缩方法)、色彩空间(例如 RGB 或 CMYK 等)以及描述每个像素颜色分量的位数,最后是构成图像的字节数据等详细信息。 |
生成 PDF 的代码只有几行。
String ImagePath = Server.MapPath("myimage.jpg"); //file path to image source
ImageDict I1 = new ImageDict(); //new image dictionary object
I1.CreateImageDict("I1", ImagePath); //create the object which describes
//the image
page.AddImageResource(I1.PDFImageName, I1, content.objectNum); //which object
//within the PDF contains the image data
PageImages pi = new PageImages();
content.SetStream(pi.ShowImage("I1", 300, 700, 144, 100)); //draw an image
//called 'I1', where and what size
一旦我们创建了数据,我们就需要将其写入物理 PDF 文件。
file.Write(I1.GetImageDict(file.Length, out size), 0, size);
添加图像的代码
首先,我们需要定义图像在文件系统中的位置。
其次,我们需要将此数据以对象(上表中的 9 0)的形式添加到 PDF 中。这可能是该过程中最复杂的部分。幸运的是,Zainu 已经完成了大部分繁重的工作,创建了一个框架来跟踪对象编号和 PDF 的其他主要部分。我只是添加了一些专门用于处理图像的方法。
为了创建包含图像数据的对象,我们必须记住 PDF 文件本质上是二进制的。标记是作为 Unicode(16 位)创建的,而表示图像的实际数据在我示例的情况下只有 8 位。这意味着我们必须以略有不同的方式处理字节输出以创建对象字节。
本质上,我们将此过程分为三个部分。第一部分是发送对象的第一部分(obj X 0 .... stream),将其转换为字节数据 `imageDictStart`,然后是图像的实际字节数据 `imagebytes`,最后是对象的最后部分(endstream endobj)`imageDictEnd`,发送到 PDF 流。
这在 `GetImageDict` 和 `GetImageBytes` 方法中进行了编码。
`CreateImageDict` 以位图形式打开 jpg 以获取像素尺寸,然后将字节数据放入一个数组。最后,它将图像名称、像素尺寸等参数添加到字符串 `imageDictStart` 中,以便稍后写入页面。
接下来,我们需要将此对象的引用添加到页面索引中。这是上面示例中的标记 `/XObject <</I1 10 0 R >>`。
这在 `AddImageResource` 中写入字符串 `imageRef`,该字符串稍后由 `GetPageDict` 用于创建 PDF 页面索引。
现在所有繁重的工作都已完成,我们只需要让 PDF 知道我们想在页面上显示图像。这可以通过标记来实现,例如:
q
144 0 0 100 300 700 cm
1 0 0 1 0 0 cm
/I1 Do
Q
并使用 `PageImages.ShowImage` 添加到 PDF 中。
content.SetStream(pi.ShowImage("I1", 300, 700, 144, 100));
这可以直接写入内容流,但是我将其实现为一个单独的类,以防我以后想添加一些图像变换。
第一个参数是图像名称,第二个参数是图像应放置的(x,y)坐标,最后一个参数是图像在页面上的宽度和高度。
您现在应该已经在 PDF 中添加了图像。
其他图像类型
我只在 RGB JPG 上进行了测试。其他图像类型具有不同的压缩方法,因此具有不同的解压缩方法、不同的色彩空间和不同的比特分量级别。如果您想尝试使用其他图像类型的方法,可以从更改 `CreateImageDict` 中的解码方法开始。我认为您可以在 PDF 手册中找到所需信息。如果您有任何发现,请在下面的讨论中发布。
将图像显示为 Web 应用程序的一部分
既然我们有了 PDF,我们可能想在某个地方显示它。
在许多情况下,我们可以通过链接到文件本身来做到这一点。这对许多应用程序来说都足够了,但如果我们要限制某些用户访问该文件怎么办?
您可以在网络上设置文件权限,但这不适用于公共网站(例如)。
我决定使用一种非常简单的技术,即使用 `
但是,还有另一种使用方法。不是指定实际的 pdf 文件,而是指定一个 .aspx 文件,该文件将提供文件的字节数据。在这种情况下,我们可以确定请求者是谁,并确定他们是否有权在提供文件之前查看该文件。
如果您查看 `default.aspx` 的底部,您会看到 `
看看 `streampdf.asp` 生成的 HTML 结构。它不发送任何标头等,只发送应用程序类型以及文件的字节。
此版本仅发送硬编码文件名的字节,但是,您可以指定一个指向数据库中可能包含实际文件名/路径的引用。您甚至可以将 PDF 的二进制数据存储在数据库中。这样,您就可以控制谁可以看到该文件。
请注意,.NET 2.0 Web 应用程序有一个名为 App_Data 的特殊文件夹。它专门用于存储此类文件,因为任何浏览此文件夹中文件的人都会收到一条消息,称“系统找不到指定的文件。”
进一步开发?
我曾尝试使用 flate 压缩来减小页面字典的大小,但迄今为止不成功。我了解到 MS 对 flate 压缩算法的实现与 Adobe 的实现不同。如果有人设法弄清楚如何使用它,请发布出来!
使用其他图像格式(gif、png、tif 等)的能力也将是一个优势。如果有人成功添加了该功能,请在此发布。
如果您想使用此代码……
本着 CodeProject 的精神 - 请继续使用此代码,没有许可条件。请记住,代码是“按原样”提供的,如果它破坏了您的应用程序,我不承担任何责任。如果您成功地在应用程序中使用此代码,我将不胜感激您提及它,或者只需给我发一封电子邮件告知我它有效!
历史
2007 年 5 月 - 文章首次发布。