Friday, September 16, 2011

Estrarre immagini da PDF o convertire pagine PDF in immagini in C#

E' stata dura. Molto dura. Una nuova sfida: Estrarre immagini da un PDF o convertire una pagina PDF in una immagine. Con C#.

Ho dovuto guardarmi parecchi forum e blog per arrivare ad una soluzione decente, free e con poco sforzo.

Partiamo dal primo caso: Estrazione di una immagine da una pagina di un documento PDF.
Ho utilizzato iTextSharp, che normalmente usiamo per generare PDF, ma che è stato utile per leggere il file e trovare in esso le immagini.

Il metodo qui sotto ritorna lo stream dell'immagine.. perché a me serviva così.
Vediamo il codice (dopo aver referenziato la DLL di iTextSharp):

using iTextSharp.text;
using iTextSharp.text.pdf;

static Stream ExtractImagesFromPDF(string sourcePdf)
{
  System.IO.MemoryStream streamImage = new MemoryStream();
  // NOTE: This will only get the first image it finds per page.
  PdfReader pdf = new PdfReader(sourcePdf);
  RandomAccessFileOrArray raf = new iTextSharp.text.pdf.RandomAccessFileOrArray(sourcePdf);
  int numbOfPages = 1; //or you can set = pdf.NumberOfPages and do it foreach page;
  try
  {
    PdfDictionary pg = pdf.GetPageN(numbOfPages);
    PdfDictionary res = (PdfDictionary)PdfReader.GetPdfObject(pg.Get(PdfName.RESOURCES));
    PdfDictionary xobj = (PdfDictionary)PdfReader.GetPdfObject(res.Get(PdfName.XOBJECT));

    if (xobj != null)
    {
      foreach (PdfName name in xobj.Keys)
      {
        PdfObject obj = xobj.Get(name);
        if (obj.IsIndirect())
        {
          PdfDictionary tg = (PdfDictionary)PdfReader.GetPdfObject(obj);
          PdfName type = (PdfName)PdfReader.GetPdfObject(tg.Get(PdfName.SUBTYPE));
          if (PdfName.IMAGE.Equals(type))
          {
            int XrefIndex = Convert.ToInt32(((PRIndirectReference)obj).Number.ToString(System.Globalization.CultureInfo.InvariantCulture));
            PdfObject pdfObj = pdf.GetPdfObject(XrefIndex);
            PdfStream pdfStrem = (PdfStream)pdfObj;
            byte[] bytes = PdfReader.GetStreamBytesRaw((PRStream)pdfStrem);
            if ((bytes != null))
            {
              streamImage = new System.IO.MemoryStream(bytes);
              break;
            }
          }
       }
    }
   }
 }
 catch(Exception ex)
 { 
   Console.Write("Error extracting image from PDF : " + ex.Message);
 } 
 finally
 {
   pdf.Close();
 }
 return streamImage;
}

Quello che fa non è altro che leggere la prima pagina (ma è possibile ciclare e salvarsi un array di immagini) ed estrarre l'oggetto di tipo IMAGE del PDF.
Ma, ho incontrato un po' di difficoltà.
Innanzitutto, si presuppone che il PDF sia "ben fatto", quindi abbia effettivamente la struttura che leggiamo.
Questa struttura, ad es., viene correttamente generata dal "Save as PDF" di MS Office 2007/2010.
Purtroppo, però, alcuni PDF non sono così semplici da trattare.
Ad es., ho un PDF che ha una immagine grande: probabilmente generato da "Save as PDF" da Photoshop: non ha quindi la struttura letta comodamente con iTextSharp (ovvero è nullo l'XObject del PDF).

Allora, passiamo al secondo metodo: Convertire una pagina del PDF in una immagine.
Per questo progettino, ho utilizzato le librerie e il codice di Cyotek: http://cyotek.com/blog/convert-a-pdf-into-a-series-of-images-using-csharp-and-ghostscript

I passi da seguire sono abbastanza ben descritti. Li ripeto come memorandum:
- Download e install dei GhostScript http://www.ghostscript.com/ : copiare la DLL da copiare nella root dell'applicazione o nella bin.
- Download dei due progetti forniti da Cyotek: Cyotek.GhostScript (classe che espone i metodi di interfaccia con GS) e Cyoteck.GhostScript.PDFConversion (classe che espone i metodi di conversione).
- Estrarre i due progetti, compilarli e aggiungere le DLL nelle referenze del nostro progetto.
- E ora due righe di codice per utilizzarlo:

static System.Drawing.Bitmap ConvertPdfToImage(string sourcePdf)
{
    MemoryStream streamImage = new MemoryStream();
    //conversione for gs
    Pdf2ImageSettings settings;
    settings = new Pdf2ImageSettings();
    settings.AntiAliasMode = AntiAliasMode.High;
    settings.Dpi = 300;
    settings.GridFitMode = GridFitMode.Topological;
    settings.ImageFormat = ImageFormat.Jpeg;
    settings.TrimMode = PdfTrimMode.CropBox;

    System.Drawing.Bitmap img = new Pdf2Image(sourcePdf, settings).GetImage()
}

Bene.
I due metodi insieme mi hanno permesso di ottenere quanto cercavo.
E il cliente è contento. Spero!