Compressione PDF con iTextSharp

Attualmente sto cercando di ricomprimere un pdf che è già stato creato, sto cercando di trovare un modo per ricomprimere le immagini che sono nel documento, per ridurre le dimensioni del file.

Ho cercato di farlo con le librerie DataLogics PDE e iTextSharp, ma non riesco a trovare un modo per eseguire la ricompressione dello stream degli elementi.

Ho pensato di fare il looping degli xobject e ottenere le immagini e poi abbassare il DPI a 96 o utilizzare l’implavation C # di libjpeg per cambiare la qualità dell’immagine ma riportarla sempre nel stream pdf sembra finire sempre, con la corruzione della memoria o qualche altro problema.

Tutti i campioni saranno apprezzati

Grazie

iText e iTextSharp hanno alcuni metodi per sostituire oggetti indiretti. In particolare c’è PdfReader.KillIndirect() che fa quello che dice e PdfWriter.AddDirectImageSimple(iTextSharp.text.Image, PRIndirectReference) che puoi usare per sostituire ciò che hai ucciso.

Nel codice pseudo C # dovresti fare:

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

Convertire i byte grezzi in un’immagine .Net può essere complicato, lo lascio a te o puoi cercare qui. Mark ha una buona descrizione qui . Inoltre, tecnicamente i PDF non hanno un concetto di DPI, che è soprattutto per le stampanti. Vedere la risposta qui per ulteriori informazioni.

Usando il metodo sopra il tuo algoritmo di compressione puoi effettivamente fare due cose, restringere fisicamente l’immagine e applicare la compressione JPEG. Quando si rimpicciolisce fisicamente l’immagine e la si aggiunge, occuperà la stessa quantità di spazio dell’immagine originale ma con meno pixel con cui lavorare. Questo ti porterà a quello che consideri essere la riduzione DPI. La compressione JPEG parla da sola.

Di seguito è riportata un’app completa di C # 2010 WinForms che utilizza iTextSharp 5.1.1.0. Prende un JPEG esistente sul desktop chiamato “LargeImage.jpg” e crea un nuovo PDF da esso. Quindi apre il PDF, estrae l’immagine, la riduce fisicamente al 90% delle dimensioni originali, applica la compressione JPEG all’85% e la riporta al PDF. Vedi i commenti nel codice per maggiori informazioni. Il codice richiede molto più null / controllo degli errori. Cerca anche i commenti NOTE cui è necessario espandersi per gestire altre situazioni.

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

Non so iTextSharp, ma devi riscrivere un file PDF se qualcosa è cambiato, poiché contiene una tabella xref (indice) con la posizione esatta del file di ogni object. Ciò significa che se viene aggiunto o rimosso anche un solo byte, il PDF viene danneggiato.

La soluzione migliore per ricomprimere le immagini è JBIG2 se sono in B / N o altrimenti in JPEG2000, per cui la libreria Jasper codificherà felicemente i codestream JPEG2000 per il posizionamento in file PDF con qualsiasi qualità tu desideri.

Se fossi in me, farei tutto da codice senza le librerie PDF. Basta trovare tutte le immagini (qualsiasi cosa tra stream ed endstream dopo un JPXDecode di JPXDecode (JPEG2000), JBIG2Decode (JBIG2) o DCTDecode (JPEG)) estrailo, reencodialo con Jasper, quindi rimettilo in posizione e aggiorna la tabella xref.

Per aggiornare la tabella xrif, trovare le posizioni di ciascun object (a partire da 00001 0 obj ) e aggiornare le nuove posizioni nella tabella xrif. Non è troppo lavoro, meno di quello che sembra. Potresti essere in grado di ottenere tutti gli offset con una singola espressione regolare (non sono un programmatore C #, ma in PHP sarebbe così semplice).

Infine, aggiorna il valore del tag startxref nel trailer con l’offset dell’inizio della tabella xref (dove dice xref nel file).

Altrimenti finirai per decodificare l’intero PDF e riscriverlo tutto, il che sarà lento e potresti perdere qualcosa lungo il percorso.

C’è un esempio su come trovare e sostituire le immagini in un PDF esistente dal creatore di iText . In realtà è un piccolo estratto dal suo libro . Dato che è in Java, ecco una semplice sostituzione:

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

Noterai che gestisce solo JPEG. La logica è invertita (invece di gestire esplicitamente solo DCTDECODE / JPEG) in modo da poter decommentare alcuni dei tipi di immagine ignorati e sperimentare con PdfImageObject nel codice sopra. In particolare, la maggior parte delle immagini FLATEDECODE (.bmp, .png e .gif) sono rappresentate come PNG (confermate nel metodo PdfImageObject codice sorgente PdfImageObject ). Per quanto ne so, .NET non supporta la codifica PNG. Ci sono alcuni riferimenti per supportare questo qui e qui . Puoi provare un eseguibile di ottimizzazione PNG standalone, ma devi anche capire come impostare PdfName.BITSPERCOMPONENT e PdfName.COLORSPACE nel PRStream .

Per completezza, dal momento che la tua domanda riguarda specificamente la compressione PDF, ecco come si comprime un 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(); 

Si potrebbe anche provare a eseguire il PDF tramite PdfSmartCopy per ridurre le dimensioni del file. Rimuove le risorse ridondanti, ma come la chiamata a RemoveUnusedObjects() nel blocco finally , può o non può aiutare. Dipenderà da come è stato creato il PDF.

IIRC iText [Sharp] non si comporta bene con JBIG2DECODE , quindi il suggerimento di @ Alasdair sembra buono - se vuoi prendere il tempo per imparare la libreria di Jasper e usare l'approccio della forza bruta.

In bocca al lupo.

EDIT - 2012-08-17 , commento di @Craig:

Per salvare il PDF dopo aver compresso i jpeg usando il metodo ReduceResolution() sopra:

un. Istanziare un object PdfReader :

 PdfReader reader = new PdfReader(pdf); 

b. Passa il PdfReader al metodo ReduceResolution() sopra.

c. Passa il PdfReader modificato ad un PdfStamper . Ecco un modo 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(); } 

Oppure puoi usare qualsiasi altro Stream se non hai bisogno di tenere il PDF in memoria. Ad esempio, usa FileStream e salva direttamente su disco.

Ho scritto una biblioteca per fare proprio questo. Inoltre, eseguirà l’OCR dei pdf utilizzando Tesseract o Cuneiform e creerà file PDF compresso e ricercabili. È una libreria che utilizza diversi progetti open source (iTextsharp, encoder jbig2, Aforge, muPDF #) per completare l’attività. Puoi verificarlo qui http://hocrtopdf.codeplex.com/

Non sono sicuro che tu stia considerando altre librerie, ma puoi facilmente ricomprimere le immagini esistenti usando la libreria Docotic.Pdf (Dichiarazione di non responsabilità: lavoro per la società).

Ecco alcuni esempi di codice:

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

Esistono anche i metodi RecompressWithFlate , RecompressWithGroup3Fax , RecompressWithJpeg e Uncompress .

La libreria convertirà le immagini a colors in quelle bilevel se necessario. È ansible specificare il livello di compressione deflate, la qualità JPEG ecc.

Vi chiedo anche di pensarci due volte prima di usare l’approccio suggerito da @Alasdair. Se hai intenzione di trattare file PDF che non sono stati creati da te, l’attività è molto più complessa di quanto possa sembrare.

Per cominciare, ci sono un sacco di immagini compresse da codec diversi da JPXDecode , JBIG2Decode o DCTDecode . E PDF può anche contenere immagini in linea.

I file PDF salvati usando le versioni più recenti di standard (1.5 o più recenti) possono contenere flussi di riferimenti incrociati. Significa che leggere e aggiornare tali file è più complesso del semplice trovare / aggiornare alcuni numeri alla fine del file.

Quindi, per favore, usa una libreria PDF.

Un modo semplice per comprimere PDF è l’utilizzo di gsdll32.dll (Ghostscript) e Cyotek.GhostScript.dll (wrapper):

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