65.9K
CodeProject 正在变化。 阅读更多。
Home

在 PDF 中添加图像和文本框

starIconstarIconstarIconstarIconstarIcon

5.00/5 (26投票s)

2007年5月2日

CPOL

15分钟阅读

viewsIcon

269647

downloadIcon

5358

一个轻量级的 C# 库,可以动态地向 PDF 添加图像和“圆角矩形”,然后安全地将 PDF 嵌入网页

引言

Screenshot - screenshot.jpg

我需要一种动态创建 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 并将其显示在网页中。

一切如何运作

Screenshot - bezier.jpg

让我们先看看“圆角矩形”。

它们基于“三次贝塞尔曲线”。如果您曾经使用过 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

Screenshot - figure2.jpg

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
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
现在我们需要向 PDF 提供一些关于在页面上何处放置图像的信息。q 和 Q 表示我们正在使用图形光标(有关语法的完整详细信息,请参阅 PDF 手册)。

接下来的三行描述了图像相对于页面的位置、页面宽度和高度。

如果您查阅 PDF 手册,还可以对图像应用各种变换,例如旋转、缩放、倾斜以及许多其他更高级的功能。如果您想使用它们,这些就需要添加到代码中。

此标记通过 `AddImageResource` 方法作为 `GetPageDict` 方法的一部分添加到内容流中。
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
最后,我们需要描述实际的图像。

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,我们可能想在某个地方显示它。

在许多情况下,我们可以通过链接到文件本身来做到这一点。这对许多应用程序来说都足够了,但如果我们要限制某些用户访问该文件怎么办?

您可以在网络上设置文件权限,但这不适用于公共网站(例如)。

我决定使用一种非常简单的技术,即使用 `