使用iTextSharp进行PDF压缩

我目前正试图重新压缩已经创build的PDF,我试图find一种方法来重新压缩文档中的图像,以减less文件大小。

我一直在尝试使用DataLogics PDE和iTextSharp库来做到这一点,但我找不到一种方法来对这些项目进行stream再压缩。

我虽然有关于循环xobjects和获取图像,然后将DPI下降到96或使用libjpeg C#implimentation来改变图像的质量,但回到pdfstream似乎总是最终,与内存损坏或者其他一些问题。

任何样品将不胜感激。

谢谢

iText和iTextSharp有一些替代间接对象的方法。 特别是有PdfReader.KillIndirect()这样做,它和PdfWriter.AddDirectImageSimple(iTextSharp.text.Image, PRIndirectReference) ,然后你可以使用它来取代你杀了。

在伪C#代码,你会做:

 var oldImage = PdfReader.GetPdfObject(); var newImage = YourImageCompressionFunction(oldImage); PdfReader.KillIndirect(oldImage); yourPdfWriter.AddDirectImageSimple(newImage, (PRIndirectReference)oldImage); 

将原始字节转换为.Net图像可能会非常棘手,我会把它留给你,或者你可以在这里search。 马克在这里有一个很好的描述 。 另外,在技术上PDFs没有DPI的概念,大多数情况下是打印机。 在这里看到更多的答案 。

使用上面的压缩algorithm实际上可以做两件事情,实际上缩小图像以及应用JPEG压缩。 当您物理缩小图像并将其添加回来时,它将占用与原始图像相同的空间量,但使用较less的像素。 这会让你认为你是DPI减less。 JPEG压缩说明了一切。

以下是针对iTextSharp 5.1.1.0的完整C#2010 WinForms应用程序。 它需要桌面上的一个名为“LargeImage.jpg”的JPEG,并从中创build一个新的PDF。 然后打开PDF,提取图像,将其缩小到原始大小的90%,应用85%JPEG压缩并将其写回PDF。 有关更多解释,请参阅代码中的注释。 代码需要更多空/错误检查。 还会查找“ NOTE注释,您需要扩展以处理其他情况。

 using System; using System.Drawing; using System.Drawing.Imaging; using System.Drawing.Drawing2D; using System.Windows.Forms; using System.IO; using iTextSharp.text; using iTextSharp.text.pdf; namespace WindowsFormsApplication1 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void Form1_Load(object sender, EventArgs e) { //Our working folder string workingFolder = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); //Large image to add to sample PDF string largeImage = Path.Combine(workingFolder, "LargeImage.jpg"); //Name of large PDF to create string largePDF = Path.Combine(workingFolder, "Large.pdf"); //Name of compressed PDF to create string smallPDF = Path.Combine(workingFolder, "Small.pdf"); //Create a sample PDF containing our large image, for demo purposes only, nothing special here using (FileStream fs = new FileStream(largePDF, FileMode.Create, FileAccess.Write, FileShare.None)) { using (Document doc = new Document()) { using (PdfWriter writer = PdfWriter.GetInstance(doc, fs)) { doc.Open(); iTextSharp.text.Image importImage = iTextSharp.text.Image.GetInstance(largeImage); doc.SetPageSize(new iTextSharp.text.Rectangle(0, 0, importImage.Width, importImage.Height)); doc.SetMargins(0, 0, 0, 0); doc.NewPage(); doc.Add(importImage); doc.Close(); } } } //Now we're going to open the above PDF and compress things //Bind a reader to our large PDF PdfReader reader = new PdfReader(largePDF); //Create our output PDF using (FileStream fs = new FileStream(smallPDF, FileMode.Create, FileAccess.Write, FileShare.None)) { //Bind a stamper to the file and our reader using (PdfStamper stamper = new PdfStamper(reader, fs)) { //NOTE: This code only deals with page 1, you'd want to loop more for your code //Get page 1 PdfDictionary page = reader.GetPageN(1); //Get the xobject structure PdfDictionary resources = (PdfDictionary)PdfReader.GetPdfObject(page.Get(PdfName.RESOURCES)); PdfDictionary xobject = (PdfDictionary)PdfReader.GetPdfObject(resources.Get(PdfName.XOBJECT)); if (xobject != null) { PdfObject obj; //Loop through each key foreach (PdfName name in xobject.Keys) { obj = xobject.Get(name); if (obj.IsIndirect()) { //Get the current key as a PDF object PdfDictionary imgObject = (PdfDictionary)PdfReader.GetPdfObject(obj); //See if its an image if (imgObject.Get(PdfName.SUBTYPE).Equals(PdfName.IMAGE)) { //NOTE: There's a bunch of different types of filters, I'm only handing the simplest one here which is basically raw JPG, you'll have to research others if (imgObject.Get(PdfName.FILTER).Equals(PdfName.DCTDECODE)) { //Get the raw bytes of the current image byte[] oldBytes = PdfReader.GetStreamBytesRaw((PRStream)imgObject); //Will hold bytes of the compressed image later byte[] newBytes; //Wrap a stream around our original image using (MemoryStream sourceMS = new MemoryStream(oldBytes)) { //Convert the bytes into a .Net image using (System.Drawing.Image oldImage = Bitmap.FromStream(sourceMS)) { //Shrink the image to 90% of the original using (System.Drawing.Image newImage = ShrinkImage(oldImage, 0.9f)) { //Convert the image to bytes using JPG at 85% newBytes = ConvertImageToBytes(newImage, 85); } } } //Create a new iTextSharp image from our bytes iTextSharp.text.Image compressedImage = iTextSharp.text.Image.GetInstance(newBytes); //Kill off the old image PdfReader.KillIndirect(obj); //Add our image in its place stamper.Writer.AddDirectImageSimple(compressedImage, (PRIndirectReference)obj); } } } } } } } this.Close(); } //Standard image save code from MSDN, returns a byte array private static byte[] ConvertImageToBytes(System.Drawing.Image image, long compressionLevel) { if (compressionLevel < 0) { compressionLevel = 0; } else if (compressionLevel > 100) { compressionLevel = 100; } ImageCodecInfo jgpEncoder = GetEncoder(ImageFormat.Jpeg); System.Drawing.Imaging.Encoder myEncoder = System.Drawing.Imaging.Encoder.Quality; EncoderParameters myEncoderParameters = new EncoderParameters(1); EncoderParameter myEncoderParameter = new EncoderParameter(myEncoder, compressionLevel); myEncoderParameters.Param[0] = myEncoderParameter; using (MemoryStream ms = new MemoryStream()) { image.Save(ms, jgpEncoder, myEncoderParameters); return ms.ToArray(); } } //standard code from MSDN private static ImageCodecInfo GetEncoder(ImageFormat format) { ImageCodecInfo[] codecs = ImageCodecInfo.GetImageDecoders(); foreach (ImageCodecInfo codec in codecs) { if (codec.FormatID == format.Guid) { return codec; } } return null; } //Standard high quality thumbnail generation from http://weblogs.asp.net/gunnarpeipman/archive/2009/04/02/resizing-images-without-loss-of-quality.aspx private static System.Drawing.Image ShrinkImage(System.Drawing.Image sourceImage, float scaleFactor) { int newWidth = Convert.ToInt32(sourceImage.Width * scaleFactor); int newHeight = Convert.ToInt32(sourceImage.Height * scaleFactor); var thumbnailBitmap = new Bitmap(newWidth, newHeight); using (Graphics g = Graphics.FromImage(thumbnailBitmap)) { g.CompositingQuality = CompositingQuality.HighQuality; g.SmoothingMode = SmoothingMode.HighQuality; g.InterpolationMode = InterpolationMode.HighQualityBicubic; System.Drawing.Rectangle imageRectangle = new System.Drawing.Rectangle(0, 0, newWidth, newHeight); g.DrawImage(sourceImage, imageRectangle); } return thumbnailBitmap; } } } 

我不知道iTextSharp,但是如果有任何改变,你必须重写一个PDF文件,因为它包含一个外部参照表(索引)和每个对象的确切文件位置。 这意味着即使添加或删除了一个字节,PDF也会被破坏。

如果是B&W,或者JPEG2000,那么最好的办法是使用JBIG2,否则,Jasper库会很高兴地编码JPEG2000码stream,以便以任何您想要的质量放置到PDF文件中。

如果是我,我会从没有PDF库的代码中完成。 只要find所有的图像(在JPXDecode (JPEG2000), JBIG2Decode (JBIG2)或DCTDecode (JPEG)) endstream之后, streamendstream stream之间的任何图像DCTDecode将其拉出,用Jasper重新编码,然后再次将其重新粘贴并更新xref表。

要更新外部参照表,find每个对象的位置(从00001 0 obj开始),只需更新外部参照表中的新位置。 这不是太多的工作,不如听起来。 你可以用一个正则expression式来获得所有偏移量(我不是一个C#程序员,但在PHP中就是这么简单)。

然后,最后使用外部参照表的开始位置(文件中的外部参照)的偏移量更新startxref标记的值。

否则,你将最终解码整个PDF,并重写所有,这将是缓慢的,你可能会失去一些东西。

iText的创build者 在现有的PDF中查找和replace图像是一个例子。 这实际上是他的书中的一小段摘录。 由于它在Java中,所以这里是一个简单的replace:

 public void ReduceResolution(PdfReader reader, long quality) { int n = reader.XrefSize; for (int i = 0; i < n; i++) { PdfObject obj = reader.GetPdfObject(i); if (obj == null || !obj.IsStream()) {continue;} PdfDictionary dict = (PdfDictionary)PdfReader.GetPdfObject(obj); PdfName subType = (PdfName)PdfReader.GetPdfObject( dict.Get(PdfName.SUBTYPE) ); if (!PdfName.IMAGE.Equals(subType)) {continue;} PRStream stream = (PRStream )obj; try { PdfImageObject image = new PdfImageObject(stream); PdfName filter = (PdfName) image.Get(PdfName.FILTER); if ( PdfName.JBIG2DECODE.Equals(filter) || PdfName.JPXDECODE.Equals(filter) || PdfName.CCITTFAXDECODE.Equals(filter) || PdfName.FLATEDECODE.Equals(filter) ) continue; System.Drawing.Image img = image.GetDrawingImage(); if (img == null) continue; var ll = image.GetImageBytesType(); int width = img.Width; int height = img.Height; using (System.Drawing.Bitmap dotnetImg = new System.Drawing.Bitmap(img)) { // set codec to jpeg type => jpeg index codec is "1" System.Drawing.Imaging.ImageCodecInfo codec = System.Drawing.Imaging.ImageCodecInfo.GetImageEncoders()[1]; // set parameters for image quality System.Drawing.Imaging.EncoderParameters eParams = new System.Drawing.Imaging.EncoderParameters(1); eParams.Param[0] = new System.Drawing.Imaging.EncoderParameter( System.Drawing.Imaging.Encoder.Quality, quality ); using (MemoryStream msImg = new MemoryStream()) { dotnetImg.Save(msImg, codec, eParams); msImg.Position = 0; stream.SetData(msImg.ToArray()); stream.SetData( msImg.ToArray(), false, PRStream.BEST_COMPRESSION ); stream.Put(PdfName.TYPE, PdfName.XOBJECT); stream.Put(PdfName.SUBTYPE, PdfName.IMAGE); stream.Put(PdfName.FILTER, filter); stream.Put(PdfName.FILTER, PdfName.DCTDECODE); stream.Put(PdfName.WIDTH, new PdfNumber(width)); stream.Put(PdfName.HEIGHT, new PdfNumber(height)); stream.Put(PdfName.BITSPERCOMPONENT, new PdfNumber(8)); stream.Put(PdfName.COLORSPACE, PdfName.DEVICERGB); } } } catch { // throw; // iText[Sharp] can't handle all image types... } finally { // may or may not help reader.RemoveUnusedObjects(); } } } 

你会注意到它只是处理JPEG。 逻辑是颠倒的(而不是明确地只处理DCTDECODE / JPEG),所以你可以取消注释一些被忽略的图像types,并在上面的代码PdfImageObject进行实验。 特别是大多数FLATEDECODE图像(.bmp,.png和.gif)被表示为PNG(在PdfImageObject源代码的DecodeImageBytes方法中确认)。 据我所知,.NET不支持PNG编码。 有一些引用来支持这里和这里 。 你可以尝试一个独立的PNG优化可执行文件 ,但你也必须弄清楚如何在PdfName.COLORSPACE中设置PdfName.BITSPERCOMPONENTPRStream

为了完整起见,因为您的问题具体询问PDF压缩,下面是如何使用iTextSharp压缩PDF:

 PdfStamper stamper = new PdfStamper( reader, YOUR-STREAM, PdfWriter.VERSION_1_5 ); stamper.Writer.CompressionLevel = 9; int total = reader.NumberOfPages + 1; for (int i = 1; i < total; i++) { reader.SetPageContent(i, reader.GetPageContent(i)); } stamper.SetFullCompression(); stamper.Close(); 

您也可以尝试通过PdfSmartCopy运行PDF以缩小文件大小。 它删除了多余的资源,但是像在finally块中对RemoveUnusedObjects()的调用一样,它可能帮助也可能不帮助。 这将取决于如何创buildPDF。

IIRC iText [Sharp]对JBIG2DECODE处理不太JBIG2DECODE ,所以JBIG2DECODE的build议看起来不错 – 如果你想花时间学习Jasper库,并使用powershell方法。

祝你好运。

编辑 – 2012-08-17 ,由@Craig发表评论:

使用上面的ReduceResolution()方法压缩ReduceResolution()后保存PDF:

一个。 实例化一个PdfReader对象:

 PdfReader reader = new PdfReader(pdf); 

湾 将PdfReader传递给PdfReaderReduceResolution()方法。

C。 将更改过的PdfReader给一个PdfStamper 。 以下是使用MemoryStream的一种方法:

 // Save altered PDF. then you can pass the btye array to a database, etc using (MemoryStream ms = new MemoryStream()) { using (PdfStamper stamper = new PdfStamper(reader, ms)) { } return ms.ToArray(); } 

或者,如果您不需要将PDF保存在内存中,则可以使用其他任何Stream 。 例如使用FileStream并直接保存到磁盘。

我已经写了一个图书馆来做到这一点。 它也将使用Tesseract或Cuneiform OCR pdf,并创build可search的,压缩的PDF文件。 这是一个使用多个开源项目(iTextsharp,jbig2编码器,Aforge,muPDF#)来完成任务的库。 你可以在这里查看http://hocrtopdf.codeplex.com/

我不确定你是否正在考虑其他图书馆,但你可以轻松地使用Docotic.Pdf库重新压缩现有的图像(声明:我为公司工作)。

这是一些示例代码:

 static void RecompressExistingImages(string fileName, string outputName) { using (PdfDocument doc = new PdfDocument(fileName)) { foreach (PdfImage image in doc.Images) image.RecompressWithGroup4Fax(); doc.Save(outputName); } } 

还有RecompressWithFlateRecompressWithGroup3FaxRecompressWithJpeg Uncompress方法。

如果需要的话,图书馆会将彩色图像转换成双层图像。 您可以指定放气压缩级别,JPEG质量等

我也要求你在使用@Alasdairbuild议的方法之前三思。 如果你打算处理不是由你创build的PDF文件,那么这个任务看起来要复杂得多。

首先,除了JPXDecodeJBIG2DecodeDCTDecode以外,还有大量的图像被压缩。 PDF也可以包含内嵌图像。

使用较新版本的标准(1.5或更新版本)保存的PDF文件可以包含交叉引用stream。 这意味着读取和更新这些文件比在文件末尾find/更新一些数字要复杂得多。

所以,请使用PDF库。

压缩PDF的简单方法是使用gsdll32.dll(Ghostscript)和Cyotek.GhostScript.dll(包装器):

 public static void CompressPDF(string sInFile, string sOutFile, int iResolution) { string[] arg = new string[] { "-sDEVICE=pdfwrite", "-dNOPAUSE", "-dSAFER", "-dBATCH", "-dCompatibilityLevel=1.5", "-dDownsampleColorImages=true", "-dDownsampleGrayImages=true", "-dDownsampleMonoImages=true", "-sPAPERSIZE=a4", "-dPDFFitPage", "-dDOINTERPOLATE", "-dColorImageDownsampleThreshold=1.0", "-dGrayImageDownsampleThreshold=1.0", "-dMonoImageDownsampleThreshold=1.0", "-dColorImageResolution=" + iResolution.ToString(), "-dGrayImageResolution=" + iResolution.ToString(), "-dMonoImageResolution=" + iResolution.ToString(), "-sOutputFile=" + sOutFile, sInFile }; using(GhostScriptAPI api = new GhostScriptAPI()) { api.Execute(arg); } }