2012-01-05 722 views
9

Actualmente estoy tratando de volver a comprimir un PDF que ya se ha creado, estoy tratando de encontrar una manera de recomprimir las imágenes que están en el documento, para reducir el tamaño del archivo.Compresión PDF con iTextSharp

He estado tratando de hacer esto con las bibliotecas DataLogics PDE e iTextSharp, pero no puedo encontrar una forma de hacer la recompresión de flujo de los elementos.

Tengo pensado sobre el bucle sobre los objetos xobjects y obtener las imágenes y luego bajar el DPI a 96 o usar el libjpeg C# implimentation para cambiar la calidad de la imagen pero volver a meterla en la secuencia de PDF parece terminar siempre , con corrupción de memoria o algún otro problema.

Cualquier muestra será apreciada.

Gracias

+0

ver este http://stackoverflow.com/questions/5296667/pdftk-compression-option también podría usar ImageMagick – Guillaume

+0

ya que es .NET, el problema @Guillaume está hablando de http://imagemagick.codeplex.com/ – balexandre

+0

@balexandre El problema no es el remuestreo de las imágenes, sino que volviendo a poner las imágenes en la secuencia de pdf, no puede guardar las imágenes en el disco, ya que creará problemas con la transparencia, etc. – user1053237

Respuesta

7

no saber sobre iTextSharp, pero hay que reescribir un archivo PDF si se cambia nada, ya que contiene una tabla de referencias externas (índice) con la posición exacta del archivo de cada objeto. Esto significa que si se agrega o elimina un solo byte, el PDF se corrompe.

Su mejor apuesta para volver a comprimir las imágenes JBIG2 es si son B & W, o JPEG2000 lo contrario, para los cuales la biblioteca Jasper estará feliz de codificar JPEG2000 codestreams para la colocación en archivos PDF en cualquier calidad así lo desea.

Si fuera yo, lo haría todo desde el código sin las bibliotecas de PDF. Solo encuentre todas las imágenes (cualquier cosa entre stream y endstream después de una ocurrencia de JPXDecode (JPEG2000), JBIG2Decode (JBIG2) o DCTDecode (JPEG)) sáquelo, vuelva a codificarlo con Jasper, luego vuelva a insertarlo y actualice la tabla de referencias externas.

Para actualizar la tabla xref, busque las posiciones de cada objeto (comenzando por 00001 0 obj) y simplemente actualice las nuevas posiciones en la tabla xref. No es demasiado trabajo, menos de lo que parece. Es posible que pueda obtener todas las compensaciones con una sola expresión regular (no soy un programador de C#, pero en PHP sería así de simple.)

Luego, finalmente actualice el valor de la etiqueta startxref en el trailer con el desplazamiento del comienzo de la tabla xref (donde dice xref en el archivo).

De lo contrario, terminará descodificando todo el PDF y reescribiéndolo todo, lo que será lento y puede perder algo en el camino.

+0

aparece el problema donde realmente necesita asegurarse de que la transmisión no esté corrupta – user1053237

+0

¿Qué secuencia, la secuencia de imágenes? ¿Por qué la transmisión sería corrupta? – Alasdair

+0

Bueno, para realmente obtenerlo correctamente, en primer lugar traté de guardar las partes entre el flujo y la corriente final en un archivo de imagen en el disco y no se pudo abrir. Ahora solo estoy escribiendo cada línea en un nuevo archivo pdf y luego comprobando si puedo obtener la línea que contiene la imagen en un flujo de memoria o algo similar – user1053237

8

iText e iTextSharp tienen algunos métodos para reemplazar objetos indirectos. Específicamente, hay PdfReader.KillIndirect() que hace lo que dice y PdfWriter.AddDirectImageSimple(iTextSharp.text.Image, PRIndirectReference) que luego puede usar para reemplazar lo que eliminó.

En seudo código C# que haría:

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

la conversión de los bytes sin una imagen de .Net a puede ser complicado, voy a dejar que depende de usted o puede buscar aquí. Mark tiene un good description here. Además, técnicamente los archivos PDF no tienen un concepto de DPI, que es principalmente para impresoras. See the answer here para obtener más información al respecto.

Utilizando el método anterior, su algoritmo de compresión puede hacer dos cosas, reducir físicamente la imagen y aplicar compresión JPEG. Cuando reduce físicamente la imagen y la vuelve a agregar, ocupará la misma cantidad de espacio que la imagen original pero con menos píxeles para trabajar.Esto le dará lo que considera que es la reducción de DPI. La compresión JPEG habla por sí misma.

A continuación se muestra una aplicación WinForms C# 2010 de trabajo completo dirigida a iTextSharp 5.1.1.0. Toma un JPEG existente en su escritorio llamado "LargeImage.jpg" y crea un nuevo PDF a partir de él. Luego abre el PDF, extrae la imagen, la reduce físicamente al 90% del tamaño original, aplica 85% de compresión JPEG y la vuelve a escribir en el PDF. Vea los comentarios en el código para más de una explicación. El código necesita mucha más comprobación de nulos/errores. También busca NOTE comentarios donde tendrá que expandirse para manejar otras situaciones.

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; 
     } 
    } 
} 
+0

Con respecto a DPI, se basa en el tamaño y la escala de la imagen frente al tamaño de la página. Por lo tanto, diferentes partes de la misma página podrían tener diferentes DPI. – Alasdair

+0

Sí, eso es efectivamente lo que sucede. Pero en ninguna parte del lenguaje PDF puede decir "hacer esta imagen 300 DPI". En cambio, dices "aquí hay una imagen de 500 píxeles de ancho, escala un 10%". –

+0

@ChrisHaas todo lo que estoy tratando de hacer es bajar el tamaño del archivo PDF – user1053237

4

No es un ejemplo de cómo find and replace images in an existing PDF por el creator of iText. En realidad es un pequeño extracto del his book. Ya que está en Java, he aquí una simple sustitución:

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(); 
    } 
    } 
} 

Se dará cuenta de que es solamente el manejo de JPEG. La lógica se invierte (en lugar de tratar explícitamente solo DCTDECODE/JPEG) por lo que puede descomentar algunos de los tipos de imágenes ignoradas y experimentar con el PdfImageObject en el código anterior. En particular, la mayoría de las imágenes FLATEDECODE (.bmp, .png y .gif) se representan como PNG (confirmadas en el método DecodeImageBytes del código fuente PdfImageObject). Por lo que sé, .NET no es compatible con la codificación PNG. Hay algunas referencias para admitir esto here y here. Puede probar un PNG optimization executable autónomo, pero también tiene que averiguar cómo configurar PdfName.BITSPERCOMPONENT y PdfName.COLORSPACE en el PRStream.

Para completarlo, ya que su pregunta se refiere específicamente acerca de la compresión PDF, aquí es cómo comprimir un archivo PDF con iTextSharp:

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(); 

También puede intentar y ejecutar el archivo PDF a través PdfSmartCopy para obtener el tamaño del archivo. Elimina recursos redundantes, pero al igual que la llamada al RemoveUnusedObjects() en el bloque finally, puede o no ayudar. Eso dependerá de cómo se creó el PDF.

IIRC iText [Sharp] no funciona bien con JBIG2DECODE, por lo que la sugerencia de @ Alasdair se ve bien, si desea tomarse el tiempo para aprender la biblioteca de Jasper y usar el enfoque de fuerza bruta.

Buena suerte.

EDITAR - 2012-08-17, comentario de @Craig:

Para guardar el PDF después de comprimir los archivos JPEG usando el método anterior ReduceResolution():

a. Una instancia de un objeto PdfReader:

PdfReader reader = new PdfReader(pdf); 

b. Pase el PdfReader al ReduceResolution() método anterior.

c. Pase el PdfReader modificado a PdfStamper.He aquí una forma usando un 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(); 
} 

O puede utilizar cualquier otro Stream si no es necesario para mantener el PDF en la memoria. P.ej. utilice un FileStream y guárdelo directamente en el disco.

+0

. Esto es bueno, pero ¿cómo guarda el pdf una vez que ha comprimido todas las imágenes? Ese bit de código queda fuera . – Craig

+1

Gracias, respuesta actualizada. – kuujinbo

+0

Gracias por el código, traté de implementarlo. Pero me sale un error. Lo publicó como una pregunta separada aquí. http://stackoverflow.com/questions/26256195/rebuild-failed-using-pdf-compression – Martin

0

No estoy seguro si está considerando otras bibliotecas, pero puede volver a comprimir las imágenes existentes usando Docotic.Pdf library (Descargo de responsabilidad: yo trabajo para la empresa).

Aquí hay un código de ejemplo:

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

     doc.Save(outputName); 
    } 
} 

También hay RecompressWithFlate, RecompressWithGroup3Fax, RecompressWithJpeg y Uncompress métodos.

La biblioteca convertirá las imágenes en color a binivel si es necesario. Puede especificar el nivel de compresión desinflado, calidad JPEG, etc.

También le pido que piense dos veces antes de usar el enfoque sugerido por @Alasdair. Si va a tratar con archivos PDF que no ha creado, la tarea es mucho más compleja de lo que parece.

Para empezar, hay una gran cantidad de imágenes comprimidas por códecs distintos de JPXDecode, JBIG2Decode o DCTDecode. Y PDF también puede contener imágenes en línea.

Los archivos PDF guardados con las versiones más recientes de estándar (1.5 o más reciente) pueden contener flujos de referencias cruzadas. Significa que leer y actualizar dichos archivos es más complejo que encontrar/actualizar algunos números al final del archivo.

Por lo tanto, utilice una biblioteca en PDF.

1

He escrito una biblioteca para hacer precisamente eso. También OCR los pdf usando Tesseract o Cuneiform y crea archivos PDF comprimidos y buscables. Es una biblioteca que utiliza varios proyectos de código abierto (iTextsharp, codificador jbig2, Aforge, muPDF #) para completar la tarea. Puede verificarlo aquí http://hocrtopdf.codeplex.com/

0

Una forma sencilla para comprimir PDF está utilizando gsdll32.dll (Ghostscript) y Cyotek.GhostScript.dll (envoltorio):

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); 
     } 
    } 
Cuestiones relacionadas