iTextSharp实现PDF压缩

10

我目前正在尝试重新压缩已经创建好的PDF文件,我在寻找一种方法来重新压缩文档中的图像以减小文件大小。

我一直在使用DataLogics PDE和iTextSharp库尝试进行操作,但是我找不到一种方法来对项目进行流重新压缩。

我考虑循环遍历xobjects并获取图像,然后将DPI降低到96或使用libjpeg C#实现来改变图像的质量,但是将其放回PDF流中似乎总是会导致内存损坏或其他问题。

非常感谢您能提供任何样例。

谢谢。


请参考此链接:https://dev59.com/T2435IYBdhLWcg3wpxyw,您也可以使用ImageMagick。 - Guillaume
由于它是.NET,@Guillaume的问题涉及到http://imagemagick.codeplex.com/。 - balexandre
@balexandre 问题不在于重新采样图像,而是将图像重新加入到PDF流中,您不能将图像保存到磁盘上,因为这会导致透明度等问题。 - user1053237
但是您可以将其保存到 MemoryStream 中,然后将其作为附加项或添加带有该 Stream 的页面返回文档,对吗? - balexandre
@balexandre 嗯,是的,但我需要重新将其注入到流中,而不是使用位图。 - user1053237
6个回答

11
iTextiTextSharp有一些替换间接对象的方法。具体来说,有一个叫做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图像可能有些棘手,我会留给您处理或您可以在此处搜索。马克在这里有一个很好的描述。此外,从技术上讲,PDF没有DPI的概念,那主要是针对打印机的。在这里查看更多信息

使用上面的方法,您的压缩算法实际上可以做两件事,物理缩小图像以及应用JPEG压缩。当您物理缩小图像并将其添加回去时,它将占用与原始图像相同的空间,但可用像素较少。这将使您获得您认为的DPI降低。JPEG压缩说明了一切。

以下是一个完整的工作中的C# 2010 WinForms应用程序,针对iTextSharp 5.1.1.0。它接收你桌面上现有的名为“LargeImage.jpg”的JPEG,并从中创建一个新的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;
        }
    }
}

关于DPI,它基于图像的大小和比例与页面的大小相对应。因此,同一页的不同部分可能具有不同的DPI。 - Alasdair
是的,这实际上就是发生的事情。但在 PDF 语言中,你不能说“使此图像为 300 DPI”。相反,你说:“这是一个宽度为 500 像素的图像,请将其缩放10%。” - Chris Haas
@ChrisHaas,我实际上想做的就是缩小PDF文件的大小。 - user1053237
同时,使用这种方法会在原本应该是透明的地方得到白色碎片。 - user1053237
所以我认为实际问题是如何在重新压缩内存流时保留透明度。 - user1053237
@user1053237,如果您想使用iTextSharp缩小PDF,则缩小图像是最好的方法,以下是如何操作。 iTextSharp本身不会触及您的图像或为您应用任何压缩。(好吧,从技术上讲,它将应用一定程度的无损压缩,但这不会对已经压缩的数据产生太大影响。)上面的示例展示了如何处理不支持透明度的JPG。 您应该能够执行相同的基本例程来处理PNG,但您需要在“ConvertImageToBytes”中切换编码器。 - Chris Haas

7
我不了解iTextSharp,但如果PDF文件有任何变化,你必须重写它,因为它包含一个xref表(索引),每个对象的确切文件位置都在其中。这意味着,即使添加或删除一个字节,PDF也会损坏。
如果要重新压缩图像,最好使用JBIG2(黑白图像)或JPEG2000(其他类型的图像)。对于后者,Jasper库可以轻松编码JPEG2000码流,以便将其放置到PDF文件中,并按照您所需的质量进行设置。
如果是我,我会全部使用代码来实现,而不需要使用PDF库。只需找到所有图像(出现JPXDecode(JPEG2000),JBIG2Decode(JBIG2)或DCTDecode(JPEG)之后的和之间的任何内容),将其取出,用Jasper重新编码,然后再将其放回并更新xref表。
要更新xref表,请查找每个对象(起始位为00001 0 obj)的位置,并仅在xref表中更新新位置。这并不太费力,比听上去要少。您可能能够使用一个正则表达式获得所有偏移量(我不是C#程序员,但在PHP中却那么简单)。
最后,将中标记的值更新为xref表开始处的偏移量(在文件中标为xref)。
否则,您将不得不解码整个PDF并重新编写它,这将很慢,并且您可能会在其中失去某些内容。

问题在于你需要确保流不会损坏。 - user1053237
哪个流,图像流吗?为什么流会损坏? - Alasdair
为了准确获取它,我首先尝试将流和endstream之间的部分保存到磁盘上的图像文件中,但是无法打开。现在,我只是将每行写入新的pdf文件,然后检查是否可以将包含图像的行放入内存流或其他什么东西中。 - user1053237
1
如果您正确保存它,那么它肯定可以被打开。在 streamendstream 之间有一个换行字节,您需要考虑到这一点,并在保存到文件之前从变量中删除它。不要对其使用字符串修剪函数,因为这会修剪掉您想要的一些字节并损坏图像。然后您只需要确保知道它是什么格式。嵌入式码流格式的JPEG2000没有完整的头文件,因此扩展名为 .jpc,Jasper 可以处理此格式。JPEGs(DCTDecode)应该是正常的。 - Alasdair
如果你无法从磁盘打开它,将其保存在内存中也没有帮助。这仍然是同一张图片。如果它在一个中损坏了,在另一个中也会损坏。如果当您查看 PDF 时显示图像,则可以提取该图像并保存到文件中。您的问题可能与那些换行字节有关。 - Alasdair
显示剩余3条评论

5

这里有一个关于如何在现有PDF中查找和替换图像的示例,作者是iText的创建者。实际上,这是从他的书中的一个小节摘录而来。由于它是使用Java编写的,因此这里提供一个简单的替代方案:

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),所以你可以取消注释一些被忽略的图片类型并在上面的代码中尝试PdfImageObject。特别是,大多数FLATEDECODE图像(.bmp、.png和.gif)都表示为PNG(在PdfImageObject源代码的DecodeImageBytes方法中确认)。据我所知,.NET不支持PNG编码。这里herehere有一些相关的参考资料。你可以尝试一个独立的PNG优化可执行文件,但你也必须弄清如何设置PRStream中的PdfName.BITSPERCOMPONENT和PdfName.COLORSPACE。

为了完整起见,由于您的问题特别涉及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();

你也可以尝试运行PDF通过PdfSmartCopy来缩小文件大小。它会删除冗余资源,但是像在finally块中调用RemoveUnusedObjects()一样,它可能有助于减小文件大小,也可能没有。这取决于PDF是如何创建的。
如果你想学习Jasper库并使用暴力方法,那么iText[Sharp]可能无法很好地处理JBIG2DECODE,所以@Alasdair的建议看起来不错。
祝你好运。
编辑-2012-08-17,@Craig的评论:
压缩JPEG使用ReduceResolution()方法后保存PDF:
a. 实例化一个PdfReader对象:
PdfReader reader = new PdfReader(pdf);
b.PdfReader 传递给上面的 ReduceResolution() 方法。 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并直接保存到磁盘上。

这很好,但是当你压缩了所有图像后,如何保存pdf呢?那段代码被省略了,唉。 - Craig
谢谢你提供的代码,我尝试着去实现它。但是我遇到了一个错误,已经在这里发布了一个单独的问题。http://stackoverflow.com/questions/26256195/rebuild-failed-using-pdf-compression - Martin at Mennt
这个方法很有效,将我的PDF压缩了90% - 谢谢! - Intrigue
文件写入部分不太清楚。当我使用代码 PdfStamer(reader, new FileStream(@"C:\outputfile.pdf", FileMode.Create) 进行写入时,没有任何变化。也就是说,文件仍然是原来的样子。我做错了吗? - Zeeshan
@Intrigue,你能否提及一下你的代码部分,在哪里将ReduceResolution的结果写入文件?这对我来说不起作用,或者我可能漏掉了什么。 - Zeeshan
太棒了!它确实压缩了PDF中的图像...我只是将这个void更改为返回PdfReader,基本上,我发送Reader并获得一个Reader返回...这次图像被压缩了。 - Guillermo Perez

1
我已经编写了一个库来完成这个任务。它还将使用Tesseract或Cuneiform对PDF进行OCR,并创建可搜索的压缩PDF文件。该库使用多个开源项目(iTextsharp、jbig2编码器、Aforge、muPDF#)来完成任务。您可以在此处查看http://hocrtopdf.codeplex.com/

1

我不确定您是否考虑其他库,但是您可以轻松地使用Docotic.Pdf library(免责声明:我在这家公司工作)重新压缩现有图像。

以下是一些示例代码:

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

        doc.Save(outputName);
    }
}

还有RecompressWithFlateRecompressWithGroup3FaxRecompressWithJpegUncompress方法。

如果需要,该库将把彩色图像转换为双色调图像。您可以指定deflate压缩级别、JPEG质量等。

我也要求您在使用@Alasdair建议的方法之前三思。如果您要处理不是由您创建的PDF文件,则任务比看起来复杂得多。

首先,有很多图像是由除JPXDecodeJBIG2DecodeDCTDecode之外的编解码器压缩的。而且PDF还可以包含内联图像。

使用新版本标准(1.5或更高版本)保存的PDF文件可能包含交叉引用流。这意味着读取和更新此类文件比仅在文件末尾查找/更新一些数字更复杂。

因此,请使用PDF库。


0

压缩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);
        }
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接