开发环境
| 名称 | 版本 |
|---|---|
| 操作系统 | Windows 10 X64 |
| Visual Studio Ultimate | 12.0.21005.1 REL |
| iText | itext7.7.1.2 |
参考
源码下载
需要引用的 dll
下载地址:
链接:https://share.weiyun.com/CZJPfYvG
密码:jgdh6g
Common.Logging.3.4.1Common.Logging.Core.3.4.1itext7.7.1.2itext7.font-asian.7.1.2iTextSharp.5.5.13Portable.BouncyCastle.1.8.1.3
PDF 工具类
加密类型
/// <summary>/// 加密类型/// </summary>public enum EncryptionType{AES_256 = EncryptionConstants.ENCRYPTION_AES_256,AES_128 = EncryptionConstants.ENCRYPTION_AES_128,RC4_128 = EncryptionConstants.STANDARD_ENCRYPTION_128,RC4_40 = EncryptionConstants.STANDARD_ENCRYPTION_40}
PDF 参数类
/// <summary>/// PDF 参数类/// </summary>public class PDFParams{/// <summary>/// PDF 模板路径/// </summary>public string TemplatePath;/// <summary>/// PDF 模版字节/// </summary>public byte[] TempLateByte;/// <summary>/// PDF 存放的路径/// </summary>public string FileDirectory;/// <summary>/// 默认字体大小/// </summary>//public int DefaultFontSize = 12;public int DefaultFontSize = 32;/// <summary>/// 默认字体:宋体/// </summary>public string DefaultFontPath = @"C:\Windows\Fonts\simsun.ttc,0";/// <summary>/// 需要添加水印的 X 坐标/// </summary>public float X { get; set; }/// <summary>/// 需要添加水印的 Y 坐标/// </summary>public float Y { get; set; }/// <summary>/// 应用于文本的旋转角度,以弧度为单位/// 为 0 则不旋转/// </summary>public float Angle { get; set; }/// <summary>/// 水印文字/// </summary>public string Watermark { get; set; }/// <summary>/// 水印文字-RBG 值-R 值/// </summary>public int R = 210;/// <summary>/// 水印文字-RBG 值-G 值/// </summary>public int G = 210;/// <summary>/// 水印文字-RBG 值-B 值/// </summary>public int B = 210;/// <summary>/// 打开文档时输入的密码/// </summary>public string userPassword;/// <summary>/// 用户编辑 PDF 时需要的密码/// </summary>public string ownerPassword;}
生产字节码文件需要用的类-MemoryTributary
/// <summary>/// MemoryTributary is a re-implementation of MemoryStream that uses a dynamic list of byte arrays as a backing store, instead of a single byte array, the allocation/// of which will fail for relatively small streams as it requires contiguous memory./// </summary>public class MemoryTributary : Stream /* http://msdn.microsoft.com/en-us/library/system.io.stream.aspx */{#region Constructorspublic MemoryTributary(){Position = 0;}public MemoryTributary(byte[] source){this.Write(source, 0, source.Length);Position = 0;}/* length is ignored because capacity has no meaning unless we implement an artifical limit */public MemoryTributary(int length){SetLength(length);Position = length;byte[] d = block; //access block to prompt the allocation of memoryPosition = 0;}#endregion#region Status Propertiespublic override bool CanRead{get { return true; }}public override bool CanSeek{get { return true; }}public override bool CanWrite{get { return true; }}#endregion#region Public Propertiespublic override long Length{get { return length; }}public override long Position { get; set; }#endregion#region Membersprotected long length = 0;protected long blockSize = 65536;protected List<byte[]> blocks = new List<byte[]>();#endregion#region Internal Properties/* Use these properties to gain access to the appropriate block of memory for the current Position *//// <summary>/// The block of memory currently addressed by Position/// </summary>protected byte[] block{get{while (blocks.Count <= blockId)blocks.Add(new byte[blockSize]);return blocks[(int)blockId];}}/// <summary>/// The id of the block currently addressed by Position/// </summary>protected long blockId{get { return Position / blockSize; }}/// <summary>/// The offset of the byte currently addressed by Position, into the block that contains it/// </summary>protected long blockOffset{get { return Position % blockSize; }}#endregion#region Public Stream Methodspublic override void Flush(){}public override int Read(byte[] buffer, int offset, int count){long lcount = (long)count;if (lcount < 0){throw new ArgumentOutOfRangeException("count", lcount, "Number of bytes to copy cannot be negative.");}long remaining = (length - Position);if (lcount > remaining)lcount = remaining;if (buffer == null){throw new ArgumentNullException("buffer", "Buffer cannot be null.");}if (offset < 0){throw new ArgumentOutOfRangeException("offset", offset, "Destination offset cannot be negative.");}int read = 0;long copysize = 0;do{copysize = Math.Min(lcount, (blockSize - blockOffset));Buffer.BlockCopy(block, (int)blockOffset, buffer, offset, (int)copysize);lcount -= copysize;offset += (int)copysize;read += (int)copysize;Position += copysize;} while (lcount > 0);return read;}public override long Seek(long offset, SeekOrigin origin){switch (origin){case SeekOrigin.Begin:Position = offset;break;case SeekOrigin.Current:Position += offset;break;case SeekOrigin.End:Position = Length - offset;break;}return Position;}public override void SetLength(long value){length = value;}public override void Write(byte[] buffer, int offset, int count){long initialPosition = Position;int copysize;try{do{copysize = Math.Min(count, (int)(blockSize - blockOffset));EnsureCapacity(Position + copysize);Buffer.BlockCopy(buffer, (int)offset, block, (int)blockOffset, copysize);count -= copysize;offset += copysize;Position += copysize;} while (count > 0);}catch (Exception e){Position = initialPosition;throw e;}}public override int ReadByte(){if (Position >= length)return -1;byte b = block[blockOffset];Position++;return b;}public override void WriteByte(byte value){EnsureCapacity(Position + 1);block[blockOffset] = value;Position++;}protected void EnsureCapacity(long intended_length){if (intended_length > length)length = (intended_length);}#endregion#region IDispose/* http://msdn.microsoft.com/en-us/library/fs2xkftw.aspx */protected override void Dispose(bool disposing){/* We do not currently use unmanaged resources */base.Dispose(disposing);}#endregion#region Public Additional Helper Methods/// <summary>/// Returns the entire content of the stream as a byte array. This is not safe because the call to new byte[] may/// fail if the stream is large enough. Where possible use methods which operate on streams directly instead./// </summary>/// <returns>A byte[] containing the current data in the stream</returns>public byte[] ToArray(){long firstposition = Position;Position = 0;byte[] destination = new byte[Length];Read(destination, 0, (int)Length);Position = firstposition;return destination;}/// <summary>/// Reads length bytes from source into the this instance at the current position./// </summary>/// <param name="source">The stream containing the data to copy</param>/// <param name="length">The number of bytes to copy</param>public void ReadFrom(Stream source, long length){byte[] buffer = new byte[4096];int read;do{read = source.Read(buffer, 0, (int)Math.Min(4096, length));length -= read;this.Write(buffer, 0, read);} while (length > 0);}/// <summary>/// Writes the entire stream into destination, regardless of Position, which remains unchanged./// </summary>/// <param name="destination">The stream to write the content of this stream to</param>public void WriteTo(Stream destination){long initialpos = Position;Position = 0;this.CopyTo(destination);Position = initialpos;}#endregion}
核心-PDF 生成类
using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.IO;using System.Runtime.InteropServices;using System.Threading.Tasks;using iText.IO.Font;using iText.IO.Image;using iText.Barcodes;using iText.Kernel.Colors;using iText.Kernel.Font;using iText.Kernel.Pdf;using iText.Layout;using iText.Layout.Borders;using iText.Layout.Element;using iText.Layout.Properties;using iText.Barcodes.Qrcode;using iText.IO.Source;using iText.Kernel.Pdf.Annot;using iText.Kernel.Pdf.Xobject;namespace ConsoleApplication1.Utils{/// <summary>/// PDF 生成类/// </summary>public class PDFMaker{/// <summary>/// PDF 参数/// </summary>PDFParams1 pdfParams;/// <summary>/// PDF 模版/// </summary>Lazy<PdfPage> Template;/// <summary>/// 字体/// </summary>Lazy<PdfFont> defaultFont;/// <summary>/// 构造函数/// </summary>public PDFMaker1(PDFParams pdfParam){pdfParams = pdfParam;if (!string.IsNullOrWhiteSpace(pdfParams.TemplatePath)){Template = new Lazy<PdfPage>(() =>{return new PdfDocument(new PdfReader(pdfParams.TemplatePath)).GetPage(1);});}else if (pdfParam.TempLateByte != null && pdfParam.TempLateByte.Length > 0){Template = new Lazy<PdfPage>(() =>{return new PdfDocument(new PdfReader(new MemoryStream(pdfParam.TempLateByte))).GetPage(1);});}defaultFont = new Lazy<PdfFont>(() =>{PdfFont font;font= PdfFontFactory.CreateFont(pdfParams.DefaultFontPath, PdfEncodings.IDENTITY_H,true);font.SetSubset(true);return font;});}/// <summary>/// 生成 PDF 到指定路径/// </summary>/// <returns>生成成功返回 true</returns>public bool MakePDF(){string filePath = pdfParams.FileDirectory;PdfWriter writer = new PdfWriter(filePath);MakePDFInit(writer);return true;}/// <summary>/// 生成加密后的 PDF 到指定路径/// 参考:https://www.cnblogs.com/nuomibaibai/p/16635287.html/// </summary>/// <returns>生成成功返回 true</returns>public bool MakePasswordPDF() {// Set document optionsint document_options = 0;WriterProperties prop = new WriterProperties(); // Set properties of output//userPassword:打开文档时输入的密码byte[] userPassword = null;if (!String.IsNullOrWhiteSpace(pdfParams.userPassword)) {userPassword = Encoding.ASCII.GetBytes(pdfParams.userPassword);}//ownerPassword:用户编辑 PDF 时需要的密码byte[] ownerPassword = null;if (!String.IsNullOrWhiteSpace(pdfParams.ownerPassword)){ownerPassword = Encoding.ASCII.GetBytes(pdfParams.ownerPassword);}if (userPassword == null && ownerPassword == null) {throw new Exception("必须设置打开文档时输入的密码或者用户编辑 PDF 时需要的密码");}//设置密码prop.SetStandardEncryption(userPassword, ownerPassword, document_options, (int)EncryptionType.AES_256); // Enable encryptionstring filePath = pdfParams.FileDirectory;PdfWriter writer = new PdfWriter(filePath, prop);MakePDFInit(writer);return true;}/// <summary>/// 生成 PDF 字节码/// </summary>/// <returns>PDF 字节码</returns>public byte[] MakePDFStream(){MemoryTributary file = new MemoryTributary();PdfWriter writer = new PdfWriter(file);MakePDFInit(writer);file.Position = 0;byte[] bytes = new byte[file.Length];file.Read(bytes, 0, bytes.Length);return bytes;}/// <summary>/// 生成 PDF/// </summary>/// <param name="writer">PdfWriter</param>public void MakePDFInit(PdfWriter writer){PdfDocument pdfDoc = new PdfDocument(writer);Document doc = new Document(pdfDoc);doc.SetFont(defaultFont.Value);doc.SetFontSize(pdfParams.DefaultFontSize);//doc.SetFontColor(new DeviceRgb(pdfParams.R, pdfParams.G, pdfParams.B));//doc.SetStrokeColor(new DeviceRgb(pdfParams.R, pdfParams.G, pdfParams.B));//上面的 rgb 无法生效,只能生效一种颜色doc.SetFontColor(new DeviceRgb(System.Drawing.Color.FromArgb((int)pdfParams.R, (int)pdfParams.G, (int)pdfParams.B)));doc.SetBold();pdfDoc.AddPage(Template.Value.CopyTo(pdfDoc));//在当前页的基础上添加,没有这句话就是把整个 PDF 的内容覆盖掉if (pdfParams.Angle == 0){Text txt = new Text(pdfParams.Watermark);doc.ShowTextAligned(new Paragraph(txt), pdfParams.X, pdfParams.Y, TextAlignment.LEFT, VerticalAlignment.TOP);}else{doc.ShowTextAligned(pdfParams.Watermark, pdfParams.X, pdfParams.Y, TextAlignment.LEFT, VerticalAlignment.TOP, pdfParams.Angle);}doc.Close();}}}
测试
生成 PDF(不加密)
//第一种方式:生成 PDF//PDF 参数PDFParams pdfParams = new PDFParams();pdfParams.TemplatePath = "D:\\data\\105.pdf";pdfParams.FileDirectory = "D:\\data\\105-1.pdf";pdfParams.Watermark = "适用范围:xxx\n有效期:2022年5月9日11:14:36\n再次复印无效";pdfParams.X = 77f;pdfParams.Y = 692.5f;pdfParams.Angle = 0f;PDFMaker pdfMaker = new PDFMaker(pdfParams);pdfMaker.MakePDF();
生成字节码(不加密)
//第二种方式:生成字节码保存为 pdf//PDF 参数PDFParams pdfParams = new PDFParams();pdfParams.TemplatePath = "D:\\data\\105A.pdf";pdfParams.Watermark = "适用范围:xxx\n有效期:2022年5月9日11:14:36\n再次复印无效";pdfParams.X = 77f;pdfParams.Y = 692.5f;pdfParams.Angle = 4.71f;//这里适合 PDF 页面是旋转过的,比如 A4 变成了旋转过的,正常写入字体就是竖着的 A4,这个时候就需要对字体就行旋转PDFMaker pdfMaker = new PDFMaker(pdfParams);byte[] bytes = pdfMaker.MakePDFStream();File.WriteAllBytes("D:/data/105-2.pdf", bytes);
pdfParams.Angle = 0f;
pdfParams.Angle = 4.71f;
根据字节码生成(不加密)
//第三种方式:根据字节码保存为 pdf//PDF 参数PDFParams pdfParams = new PDFParams();pdfParams.TempLateByte = File.ReadAllBytes("D:\\data\\105.pdf");pdfParams.FileDirectory = "D:\\data\\105-1.pdf";pdfParams.Watermark = "适用范围:xxx\n有效期:2022年5月9日11:14:36\n再次复印无效";//纵向pdfParams.X = 45f;pdfParams.Y = 330.5f;pdfParams.Angle = 0f;PDFMaker pdfMaker = new PDFMaker(pdfParams);pdfMaker.MakePDF();
生成 PDF(加密)
//PDF 参数PDFParams pdfParams = new PDFParams();pdfParams.TemplatePath = "D:\\data\\141.pdf";pdfParams.FileDirectory = "D:\\data\\142-加密-加密.pdf";pdfParams.Watermark = "\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0保密文件,禁止外传,改文件仅用于\r\n\r\n[xx项目][xx平台]使用\r\n\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0出具给[xx集团]";//纵向pdfParams.X = 50f;pdfParams.Y = 125.5f;pdfParams.Angle = 19.5f;pdfParams.userPassword = "123";pdfParams.ownerPassword = "456";PDFMaker pdfMaker = new PDFMaker(pdfParams);pdfMaker.MakePasswordPDF();
- 打开
142-加密-加密.pdf
- 编辑
142-加密-加密.pdf
错误-itext not assignable to target type ‘Common.Logging.Configuration.LogSetting
解决思路:iText7 unable to setup logging
问题描述
当我把上面的代码应用到我的 Web 项目中时,报了下面的错误
{"ConfigurationReader Common.Logging.Configuration.DefaultConfigurationReaderreturned unknown settings instance of typeLuoma.FMS.Framework.Logging.Configuration.LogSetting"}{"Type 'Luoma.FMS.Framework.Logging.Configuration.LogSetting, Luoma.FMS.Framework, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'of parameter 'sectionResult' is not assignable to target type'Common.Logging.Configuration.LogSetting, Common.Logging, Version=3.4.1.0, Culture=neutral, PublicKeyToken=af08829b84f0328e'\r\n参数名: sectionResult\r\n实际值是 Luoma.FMS.Framework.Logging.Configuration.LogSetting。"}在 Common.Logging.Configuration.ArgUtils.Guard[T](Function`1 function, String messageFormat, Object[] args) 位置 C:\_oss\common-logging\src\Common.Logging.Portable\Logging\Configuration\ArgUtils.cs:行号 336在 Common.Logging.Configuration.ArgUtils.Guard(Action action, String messageFormat, Object[] args) 位置 C:\_oss\common-logging\src\Common.Logging.Portable\Logging\Configuration\ArgUtils.cs:行号 296在 Common.Logging.LogManager.BuildLoggerFactoryAdapter() 位置 C:\_oss\common-logging\src\Common.Logging.Portable\Logging\LogManager.cs:行号 526在 Common.Logging.LogManager.get_Adapter() 位置 C:\_oss\common-logging\src\Common.Logging.Portable\Logging\LogManager.cs:行号 225在 iText.Kernel.Colors.DeviceRgb..ctor(Single r, Single g, Single b)在 Luoma.EACC.Web.Utils.PDFMaker.MakePDFInit(PdfWriter writer) 位置 d:\luoma\Proj-EACC\Code\EAC_net\Luoma.EACC.Web\Utils\PDFMaker.cs:行号 185在 Luoma.EACC.Web.Utils.PDFMaker.MakePDFStream() 位置 d:\luoma\Proj-EACC\Code\EAC_net\Luoma.EACC.Web\Utils\PDFMaker.cs:行号 167在 Luoma.EACC.Web.Areas.SP.Controllers.ApplyController.MakeAccountLicencePDF(AccountOpeningPermitVO accountOpeningPermitVO, String companyName, String receiverUnit, String applyReason, Nullable`1 receiptDate) 位置 d:\luoma\Proj-EACC\Code\EAC_net\Luoma.EACC.Web\Areas\SP\Controllers\ApplyController.cs:行号 1741
问题分析
根据报错信息,是下面这行代码报的错误
其实很奇怪,这行代码为什么要去写日志呢
PdfDocument pdfDoc = new PdfDocument(writer);
问题原因就是我的 Web.config 中配置了下面的日志配置信息
<configuration><configSections><sectionGroup name="common"><section name="logging" type="Luoma.FMS.Framework.Logging.ConfigurationSectionHandler, Luoma.FMS.Framework"/></sectionGroup></configSections><common><logging><factoryAdapter type="LuomaFMS.Framework.Logging.Log4Net.Log4NetLoggerFactoryAdapter, Luoma.FMS.Framework"><arg key="configType" value="FILE-WATCH" /><arg key="configFile" value="~/log4net.config" /></factoryAdapter></logging></common></configuration>
注释掉这一段,也能正常运行
<common><logging><factoryAdapter type="Luoma.FMS.Framework.Logging.Log4Net.Log4NetLoggerFactoryAdapter, Luoma.FMS.Framework"><arg key="configType" value="FILE-WATCH" /><arg key="configFile" value="~/log4net.config" /></factoryAdapter></logging></common>
说明就是 PdfDocument pdfDoc = new PdfDocument(writer); 去写日志,读取了配置信息,发现不是 Common.Logging 相关的配置信息,所以报错。
问题解决
参考了 iText7 unable to setup logging
手动指定日志配置类即可
using Common.Logging;using Common.Logging.Simple;using Common.Logging.Configuration;
// create propertiesNameValueCollection properties = new NameValueCollection();properties["showDateTime"] = "true";properties["level"] = "All";// set AdapterLogManager.Adapter = new ConsoleOutLoggerFactoryAdapter(properties);
完整代码
/// <summary>/// 生成 PDF/// </summary>/// <param name="writer">PdfWriter</param>public void MakePDFInit(PdfWriter writer){//----------------------------------------------------//https://stackoverflow.com/questions/50987189/itext7-unable-to-setup-logging//new PdfDocument(writer); 会去写日志,会去 .config 文件读取 common.logging 配置,//本项目的 common.logging 是单独的,和 Common.Logging 的配置不同,所以会报错 itext not assignable to target type 'Common.Logging.Configuration.LogSetting//所以这里手动指定 LogManager,让它别去读取配置信息// create propertiesNameValueCollection properties = new NameValueCollection();properties["showDateTime"] = "true";properties["level"] = "All";// set AdapterLogManager.Adapter = new ConsoleOutLoggerFactoryAdapter(properties);//----------------------------------------------------PdfDocument pdfDoc = new PdfDocument(writer);Document doc = new Document(pdfDoc);doc.SetFont(defaultFont.Value);doc.SetFontSize(pdfParams.DefaultFontSize);doc.SetFontColor(new DeviceRgb(pdfParams.R, pdfParams.G, pdfParams.B));doc.SetStrokeColor(new DeviceRgb(pdfParams.R, pdfParams.G, pdfParams.B));doc.SetBold();pdfDoc.AddPage(Template.Value.CopyTo(pdfDoc));//在当前页的基础上添加,没有这句话就是把整个 PDF 的内容覆盖掉if (pdfParams.Angle == 0){Text txt = new Text(pdfParams.Watermark);doc.ShowTextAligned(new Paragraph(txt), pdfParams.X, pdfParams.Y, TextAlignment.LEFT, VerticalAlignment.TOP);}else{doc.ShowTextAligned(pdfParams.Watermark, pdfParams.X, pdfParams.Y, TextAlignment.LEFT, VerticalAlignment.TOP, pdfParams.Angle);}doc.Close();}}
我真的是服了,这个 PDF 的操作类库,去写日志信息干什么?服
其它-水印文字特殊格式
| 特殊文字 | 符号 |
|---|---|
| 空格 | \u00a0 |
| 换行 | \r\n |