使用iTextSharp修改PDF文件 - 大西瓜3721

时间:2024-02-25 09:20:39

使用iTextSharp修改PDF文件

最近因为工作需要,公司让我写个小程序,对大量的PDF文件(超过2500份)的页眉页脚等区域进行修改。简单地说,就是将一批PDF文档的页眉页脚进行修改,然后生成新的PDF文件。

这种事情虽然以前没做过,但没办法,谁让我们只是个程序员呢,想继续拿工资,就、就开始工作吧……

首先,Google一下相关资料,得到了以下一些信息:
1、Acrobat提供7.0版PDF的SDK;
2、国内有个由Acrobat公司支持的,专业讨论PDF的论坛:http://www.chinapdf.com/pdfbbs/index.asp
3、网上有一大堆可以操作PDF文件的库呀、程序呀的,完全收费的(Aspose)、半收费的(PDFLib,对个人使用免费,对商业应用收费)、开源的(iTextSharpPDFBox)都有;
4、PDF是公开标准的电子文档格式,全世界并不仅仅只有Acrobat公司可以发布相关的产品,其它很多、很多公司都有相关的产品。
5、国内也有一大批人在研究PDF文件,并有自己的产品,可惜的是好像大部分都是收费的,好像还不便宜。(这是第一次发现,原来国内有这么多人在研究PDF呀,长见识了)

 作为一个程序员,首先当然是试用SDK,从网上DOWN了来,好大呀,60M哦,看看附带的几篇文档,都是近千页的,还是全英文的,看得累呀。难怪网上有人说:如果看SDK的话,没有3个月,基本上写不了什么程序的。
虽然使用SDK是最正宗、最强大、最能控制每一个细节的方法,但是,这次任务时间相当紧迫(只有3-5天的时间),看来这条路是行不通了。

第二条路,试用收费的Aspose,这个东东确实不错,可惜过于复杂,对我现的任务也大材小用了。最最重要的是,估计公司不会掏钱买的(只是一次性的任务,这个程序估计以后就没多少机会可以用了),但是公司又不允许使用D版的。

算了,看开源的吧。

重量级人物出场了:iTextSharp,鼓掌。。。。。

   这个iTextSharp确实是个好东西,可以创建、读取PDF格式的文档,虽然我的需求比较简单,但我首先还是基本上、完整地看完了它的相关文档,不喜欢英文的同志,可以搜索一篇《用C#制作PDF文件全攻略》(苟安廷),这篇文章是苟先生在使用iTextSharp时的一些心得,里面虽然重点是说明如何创建PDF文件,对读取、修改PDF文件的方法略过不提,因此,对于我的任务来说,并没有太大的作用,但在这里,仍然感谢苟先生的无私奉献。

     具体使用iTextSharp的方法,我这里就不细说了,因为非常简单,仔细看看它的文档,应该都可以很轻松地创建、读取PDF文件。我这里就只说说我在使用过程中碰到的一些问题,让后来的人少走一些弯路:

1、 PDF文件从理论上来说,只要创建成功之后,就不能再修改。

    因为我需要修改原来的PDF文件,将它的页眉页脚去掉,然后换上新的页眉页脚。所以,我最开始对怎么只取得原始文件中的内容区域(是去掉了页眉、页脚、左边固定区域、右边固定区域的一个矩形区域),研究了很久。调用了其中的GetImportedPage方法,得到字符串,然后通过分析该字符串(是极其粗略的分析,因为PDF文件格式的标志太多,后面会有相关说明),去掉其中不需要的部分,再将剩下的其它部分进行保存,生成新的PDF文件。

    理论上这种方法是正确的,也比较符合我们的一般逻辑思维(因为我们对已生成的文档、程序进行修改,大多数情况下都是用类似方法,比如:对某个程序进行解密等等)。我也确实按这种方法得到了符合要求的、新的PDF文件,但随即就发现了该方法其实不具备通用性,即对某篇文件是有效的,但对另一篇文件却有可能会造成格式错位。

    因为分析PDF文件的格式是一件非常麻烦的事情,很多明明应该是在内容区域的字节,却显示在页眉处,如果我再分析到里面最细小的、每一个标志位,还不如直接看它的SDK,而且这样的话,在规定的时间里,这个程序也将完不成了。

 解决办法:

    我先研究了Acrobat里的crop,它为什么可以这么精确的剪裁呢?

    结果让我哑然失笑,原来它的crop也不是真正的剪裁,而只是把需要的剪裁掉的区域屏蔽掉了而已,如果再回到crop里,进行上、下、左、右的设置,原来看起来好像被剪裁掉的区域仍然会显示出来,呵呵,有意思。

    好的,现在心里有底了,大概知道怎么做了,这时再仔细看看iTextSharp的文档,发现有一段话以前没有注意到:

   If you have an existing PDF file that represents a form, you could copy the pages of this form and paint text at precise locations on this form. You can\'t edit an existing PDF document, by saying: for instance replace the word Louagie by Lowagie. To achieve this, you would have to know the exact location of the word Louagie, paint a white rectangle over it and paint the word Lowagie on this white rectangle. Please avoid this kind of \'patch\' work. Do your PDF editing with an Adobe product.

呵呵,跟我想的一样,就是用新的区域,把需要剪裁的区域给覆盖掉。

这就容易多了,先用iTextSharp的Template功能,把自己需要的文字、图片、表格放到Template里,然后把整个的Template加到合适的位置,即可。

哦,别忘了,得先在Template里加个白色的矩形框,放在最底层。

注:上面提到了PDF文件的格式,其实PDF文件的格式非常有趣,是的,非常有趣。相关的信息,可参考网上的《一个简单的PDF文件结构的分析》等文章。否则当你看到<BT>、<ET>、/F1、TF时,你会感觉莫明其妙的。

2、 PDF文件中的属性,不是我们一般意义上的文件的属性。

    这一点开始让我走了一段弯路,我用iTextSharp中的相关函数,在Document.Opent()之前,设置了相关的属性,如:subject/author/title等等,但奇怪的是,生成新的PDF文件中,我用一般的看某一个文件属性的方法,却没有看到预料中的属性,都是空的。

    后来,经过有经验的同事提醒,才知道:原来所谓的PDF文件的属性,是要在Acrobat Reader的某个菜单中才能看到的。

        呵呵,以前对Acrobat的应用就基本上只有对文件进行互相转换,没用过其它太多的功能,没有经验呀。

 

 

 好吧,终于要引入正题了,DHL公司将物流面单以pdf文件流的形式返回,就是我们可以保存成Pdf文件,这样就容易处理的多,我们自己的拣货面单,如果也可以保存至Pdf文件,这样就可以将这2个Pdf文件一块打印出来,这算是一个折中的解决方案。

  首先,要将拣货数据(含图片)保存至Pdf文档。技术点如下

    1, 将DataTable导出至Pdf

    2, DataTable中包含图片,也要能导出

    3, Pdf类库,字体的引入(居然需要引用字体的绝对路径,想不通)

  下面分别讲解

    1, 这点就是直接创建iTextSharp中的PdfDataTable对象,直接映射即可。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private static pdfText.pdf.PdfPTable CreatePdfPTableToPickupLabel(List<TradeDetailModel> listDetail, iTextSharp.text.Font font)
{
    pdfText.pdf.PdfPTable pdtTable = new pdfText.pdf.PdfPTable(5);
    <strong>pdtTable.WidthPercentage = 95;      //占宽度百分比:95%(这句很关键,作用是撑满整个面单)</strong>
 
    int[] colWidth = { 2, 4, 2, 2, 2 }; //设置列宽比例
    pdtTable.SetWidths(colWidth);
 
    //此处,先插入首行,即标题
    pdtTable.AddCell(new iTextSharp.text.Phrase("图片", font));
    pdtTable.AddCell(new iTextSharp.text.Phrase("基本信息", font));
    pdtTable.AddCell(new iTextSharp.text.Phrase("单价", font));
    pdtTable.AddCell(new iTextSharp.text.Phrase("数量", font));
    pdtTable.AddCell(new iTextSharp.text.Phrase("备注", font));
 
    //再插入真实拣货数据
    int rowCount = listDetail.Count;
    for (int i = 0; i < rowCount; i++)
    {
        TradeDetailModel modelDetail = listDetail[i];
 
        iTextSharp.text.Image image = PdfUtil.CreatePdfImage(modelDetail.ProductImageBytes);
        pdtTable.AddCell(image);
        pdtTable.AddCell(new iTextSharp.text.Phrase(modelDetail.ProductBase, font));
        pdtTable.AddCell(new iTextSharp.text.Phrase(modelDetail.Price.ToString() + Environment.NewLine + modelDetail.ProductID, font));
        pdtTable.AddCell(new iTextSharp.text.Phrase(modelDetail.Number.ToString() + Environment.NewLine + modelDetail.ProductSpec, font));
        pdtTable.AddCell(new iTextSharp.text.Phrase(modelDetail.Remark, font));
    }
 
    return pdtTable;
}

    2, 这点要先生成iTextSharp中Image对象才可以,然后再随生成PdfDataTable中,将Image对象插入单元格(注意:图片尺寸需要定义好)。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/// <summary>
/// 创建Pdf所需图像
/// </summary>
/// <param name="imageBytes"></param>
/// <param name="widthS"></param>
/// <param name="heightS"></param>
/// <returns></returns>
private static iTextSharp.text.Image CreatePdfImage(byte[] imageBytes, float widthS = 60f, float heightS = 60f)
{
    iTextSharp.text.Image image = iTextSharp.text.Image.GetInstance(imageBytes);
 
    //图片大小要求统一80x80,需根据实际图片       
    float perW = (float)Math.Round(widthS / image.Width, 2);
    float perH = (float)Math.Round(heightS / image.Height, 2);
    image.ScalePercent(perW * 100, perH * 100);
 
    //设置Dpi值,能够清晰些
    image.SetDpi(124, 124);
 
    return image;
}  

    3, 创建字体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/// <summary>
/// 创建Pdf所需字体
/// </summary>
/// <returns></returns>
public static iTextSharp.text.Font CreatePdfFont(float fontSize = 16F)
{
    //黑体
    string fontPath = @"C:\Windows\Fonts\simhei.ttf";
 
    iTextSharp.text.pdf.BaseFont baseFont = iTextSharp.text.pdf.BaseFont.CreateFont(fontPath,
        iTextSharp.text.pdf.BaseFont.IDENTITY_H,
        iTextSharp.text.pdf.BaseFont.NOT_EMBEDDED);
    iTextSharp.text.Font font = new iTextSharp.text.Font(baseFont, fontSize);
 
    return font;
}

    下面是调用的代码,仅供参考。注:返回的Pdf文件的绝对路径,用于后续打印

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/// <summary>
/// 生成拣货标签pdf文件
/// </summary>
/// <param name="modelTrade"></param>
/// <param name="dhlLabelFolderName"></param>
/// <returns></returns>
public static string CreatePickupLabel(TradeModel modelTrade,string dhlLabelFolderName)
{
    //参考地址
    //https://www.cnblogs.com/yangy1989/p/5300304.html
    //http://blog.csdn.net/lideyuans/article/details/51536676 (设置图片百分比)
 
    //组装,待生成的pdf文件完整路径
    string logisticsNo = modelTrade.R;
    string pdfFileName = dhlLabelFolderName + logisticsNo + "_Label.pdf";
 
    FileStream fs = new FileStream(pdfFileName, FileMode.Create);
    pdfText.Rectangle rect = new pdfText.Rectangle(0f, 0f, 1000f, 1000f);
    pdfText.Document document = new pdfText.Document(rect, 5f, 5f, 5f, 5f);
 
    //创建字体
    iTextSharp.text.Font font = PdfUtil.CreatePdfFont(34F);
 
    pdfText.pdf.PdfWriter pdfWriter = pdfText.pdf.PdfWriter.GetInstance(document, fs);
    document.Open();
 
    //增加拣货单品列表
    pdfText.pdf.PdfPTable pdfpTable = PdfUtil.CreatePdfPTableToPickupLabel(modelTrade.Details, font);
    document.Add(pdfpTable);
 
    //增加品名备注           
    pdfText.Paragraph pgraph1 = new pdfText.Paragraph(modelTrade.ItemsDescription, font);
    document.Add(pgraph1);
 
    //增加:发货期限
    font.Color = pdfText.BaseColor.RED;
    pdfText.Paragraph pgraph2 = new pdfText.Paragraph(modelTrade.ShipLimitTimeText, font);
    document.Add(pgraph2);
 
    document.Close();
    fs.Close();
 
    return pdfFileName;
}

  

  再将这2个文件,通过C#代码连续打印出来。支持连续打印多个Pdf文件

1
2
3
4
5
6
7
8
9
10
11
12
//直接调用cmd命令,实现直接打印
foreach (string printFile in listPrintFile)
{
    Process proc = new Process();
    proc.StartInfo.CreateNoWindow = false;
    proc.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
    proc.StartInfo.UseShellExecute = true;
    proc.StartInfo.FileName = printFile;
    proc.StartInfo.Verb = "print";
    proc.Start();
    proc.Close();
}

  

  好了,最后的最后,看看效果图吧。前2联是DHL返回的Pdf文件,第3联是我本地生成的Pdf文件,合并成一个完整的发货面单

 

https://www.oschina.net/p/itextsharp