ASP.NET C# – Come unire GIF, PNG, JPG, TIFF, PDF in un singolo file PDF con iTextSharp Una piccola libreria open-source per ASP.NET C# che consente di unire uno o più file GIF, PNG, JPG, TIFF e/o PDF in un singolo file PDF con iTextSharp

How to handle multipage TIFF files with ASP.NET C# (GDI+ alternative)

Oggi ho deciso di riproporre in italiano un vecchio articolo, pubblicato in lingua inglese qualche anno fa, che descriveva una libreria ASP.NET C# per convertire uno o più file immagine (in formato GIF, PNG, JPG, TIFF, PDF) in un unico file PDF multi-pagina. Una procedura che all’epoca si rivelò essere molto meno semplice del previsto per via di una serie di complicazioni e problematiche relative alle interfacce GDI+ che ASP.NET utilizza dietro le quinte per gestire le operazioni di lettura, conversione e scrittura dei formati immagine più diffusi. In dettaglio, ho dovuto scontrarmi con:

  • Una serie di problematiche legate alla gestione dei file TIFF multi-pagina (per maggiori informazioni, leggete qui).
  • Una serie di problematiche legate al resize/resample di ciascuna immagine, così da farla entrare in una pagina PDF predeterminata (nel nostro caso, A4).

Poiché gestire entrambe le problematiche utilizzando unicamente le interfacce GDI+ sarebbe stato oltremodo faticoso, ho deciso di avvalermi di un’ottima libreria open-source chiamata iTextSharp, disponibile gratuitamente attraverso NuGetSourceForge, grazie alla quale ho potuto risparmiarmi gran parte dei grattacapi.

IMPORTANTE: La versione di iTextSharp utilizzata nel codice presente in questo articolo è la 5.5.13.1, attualmente in EOL: il codice non funzionerà con le versioni successive (7+). Se siete interessati a una versione aggiornata di questo articolo con supporto per iText 7+, scriveteci una richiesta nei commenti!

Ecco il codice completo del metodo da me realizzato: come potrete vedere non si può certo definire un one-liner, ma se non altro consente di raggiungere lo scopo con una gestione piuttosto ottimizzata della memoria, cosa fondamentale quando si lavora con immagini potenzialmente di grandi dimensioni.

using System;
using iTextSharp.text;
using iTextSharp.text.pdf;
using System.IO;
using System.Drawing.Imaging;

namespace Ryadel.Components.Media
{
    public static class PDFHelper
    {
        /// <summary>
        /// Merge one or more image or document files into a single PDF
        /// Supported Formats: bmp, gif, jpg, jpeg, png, tif, tiff, pdf (including multi-page tiff and pdf files)
        /// </summary>
        public static byte[] MergeIntoPDF(params ByteArrayInfo[] infoArray)
        {
            // If we do have a single PDF file, return it without doing anything
            if (infoArray.Length == 1 && infoArray[0].FileExtension.Trim('.').ToLower() == "pdf") return infoArray[0].Data;

            // patch to fix the "PdfReader not opened with owner password" error.
            // ref.: https://stackoverflow.com/questions/17691013/pdfreader-not-opened-with-owner-password-error-in-itext
            PdfReader.unethicalreading = true;

            using (Document doc = new Document())
            {
                doc.SetPageSize(PageSize.A4);

                using (var ms = new MemoryStream())
                {
                    // PdfWriter wri = PdfWriter.GetInstance(doc, ms);
                    using (PdfCopy pdf = new PdfCopy(doc, ms))
                    {
                        doc.Open();

                        foreach (ByteArrayInfo info in infoArray)
                        {
                            try
                            {
                                doc.NewPage();
                                Document imageDocument = null;
                                PdfWriter imageDocumentWriter = null;
                                switch (info.FileExtension.Trim('.').ToLower())
                                {
                                    case "bmp":
                                    case "gif":
                                    case "jpg":
                                    case "jpeg":
                                    case "png":
                                        using (imageDocument = new Document())
                                        {
                                            using (var imageMS = new MemoryStream())
                                            {
                                                using (imageDocumentWriter = PdfWriter.GetInstance(imageDocument, imageMS))
                                                {
                                                    imageDocument.Open();
                                                    if (imageDocument.NewPage())
                                                    {
                                                        var image = iTextSharp.text.Image.GetInstance(info.Data);
                                                        image.Alignment = Element.ALIGN_CENTER;
                                                        image.ScaleToFit(doc.PageSize.Width - 10, doc.PageSize.Height - 10);
                                                        if (!imageDocument.Add(image))
                                                        {
                                                            throw new Exception("Unable to add image to page!");
                                                        }
                                                        imageDocument.Close();
                                                        imageDocumentWriter.Close();
                                                        using (PdfReader imageDocumentReader = new PdfReader(imageMS.ToArray()))
                                                        {
                                                            var page = pdf.GetImportedPage(imageDocumentReader, 1);
                                                            pdf.AddPage(page);
                                                            imageDocumentReader.Close();
                                                        }
                                                    }
                                                }
                                            }
                                        }
                                        break;
                                    case "tif":
                                    case "tiff":
                                        //Get the frame dimension list from the image of the file
                                        using (var imageStream = new MemoryStream(info.Data))
                                        {
                                            using (System.Drawing.Image tiffImage = System.Drawing.Image.FromStream(imageStream))
                                            {
                                                //get the globally unique identifier (GUID) 
                                                Guid objGuid = tiffImage.FrameDimensionsList[0];
                                                //create the frame dimension 
                                                FrameDimension dimension = new FrameDimension(objGuid);
                                                //Gets the total number of frames in the .tiff file 
                                                int noOfPages = tiffImage.GetFrameCount(dimension);

                                                //get the codec for tiff files
                                                ImageCodecInfo ici = null;
                                                foreach (ImageCodecInfo i in ImageCodecInfo.GetImageEncoders())
                                                    if (i.MimeType == "image/tiff")
                                                        ici = i;

                                                foreach (Guid guid in tiffImage.FrameDimensionsList)
                                                {
                                                    for (int index = 0; index < noOfPages; index++)
                                                    {
                                                        FrameDimension currentFrame = new FrameDimension(guid);
                                                        tiffImage.SelectActiveFrame(currentFrame, index);
                                                        using (MemoryStream tempImg = new MemoryStream())
                                                        {
                                                            tiffImage.Save(tempImg, ImageFormat.Tiff);
                                                            using (imageDocument = new Document())
                                                            {
                                                                using (var imageMS = new MemoryStream())
                                                                {
                                                                    using (imageDocumentWriter = PdfWriter.GetInstance(imageDocument, imageMS))
                                                                    {
                                                                        imageDocument.Open();
                                                                        if (imageDocument.NewPage())
                                                                        {
                                                                            var image = iTextSharp.text.Image.GetInstance(tempImg.ToArray());
                                                                            image.Alignment = Element.ALIGN_CENTER;
                                                                            image.ScaleToFit(doc.PageSize.Width - 10, doc.PageSize.Height - 10);
                                                                            if (!imageDocument.Add(image))
                                                                            {
                                                                                throw new Exception("Unable to add image to page!");
                                                                            }
                                                                            imageDocument.Close();
                                                                            imageDocumentWriter.Close();
                                                                            using (PdfReader imageDocumentReader = new PdfReader(imageMS.ToArray()))
                                                                            {
                                                                                var page = pdf.GetImportedPage(imageDocumentReader, 1);
                                                                                pdf.AddPage(page);
                                                                                imageDocumentReader.Close();
                                                                            }
                                                                        }
                                                                    }
                                                                }
                                                            }
                                                        }
                                                    }
                                                }
                                            }
                                        }
                                        break;
                                    case "pdf":
                                        using (var reader = new PdfReader(info.Data))
                                        {
                                            for (int i = 0; i < reader.NumberOfPages; i++)
                                            {
                                                pdf.AddPage(pdf.GetImportedPage(reader, i + 1));
                                            }
                                            pdf.FreeReader(reader);
                                            reader.Close();
                                        }
                                        break;
                                    default:
                                        // not supported image format:
                                        // skip it (or throw an exception if you prefer)
                                        break;
                                }
                            }
                            catch (Exception e)
                            {
                                e.Data["FileName"] = info.FileName;
                                throw e;
                            }
                        }
                        if (doc.IsOpen()) doc.Close();
                        return ms.ToArray();
                    }
                }
            }
        }        
    }
}

Questo è il codice che definisce la classe ByteArrayInfo, utilizzata come parametro di input: come si può facilmente comprendere, lo scopo di questa classe é quello di trasmettere al metodo MergeIntoPDF sia il nome file che il byte array di ciascuno dei file che si desidera “unire”.

namespace Ryadel.Components.Media
{
    /// <summary>
    /// POCO class to store byte[] and other useful informations regarding the data.
    /// </summary>
    public class ByteArrayInfo
    {
        public ByteArrayInfo(byte[] fileData, string fileName)
        {
            Data = fileData;
            FileName = fileName;
            FileExtension = System.IO.Path.GetExtension(FileName).ToLower();
        }

        public byte[] Data { get; set; }

        /// <summary>
        /// The File Name (es. "TestFile.pdf")
        /// </summary>
        public string FileName { get; set; }

        /// <summary>
        /// The File Extension, including the dot (es. ".pdf")
        /// </summary>
        public string FileExtension { get; set; }       
    }
}

Il codice del metodo principale MergeIntoPDF è piuttosto autoesplicativo: noterete senz’altro la presenza di una grande quantità di blocchi using nidificati e non, piuttosto inevitabile quando si lavora con tipi di immagine GDI + (quasi tutti IDisposable, quindi da rimuovere manualmente dalla memoria) ; il codice presenta anche un gran numero di trasformazioni (quasi sempre di tipo MemoryStream > Image > Bitmap), anch’esse necessarie per gestire correttamente l’allocazione della memoria dei molteplici oggetti temporanei necessari per “separare” le varie pagine/immagini di input e copiarle all’interno del byte array di destinazione. Il metodo restituisce infatti un array di byte, così da consentire il salvataggio del file risultante sia su FileSystem (con il metodo System.IO.File.WriteAllBytes) o altri sistemi analoghi) che all’interno di una colonna BLOB di un qualsiasi Database.

Inutile dire che il codice è pubblicato soltanto a titolo esemplificativo: sentitevi liberi di modificare l’implementazione e/o cambiare il tipo di dati restituito a seconda delle esigenze specifiche della vostra applicazione.

Per il momento è tutto: buona conversione!

AGGIORNAMENTO: nel caso in cui, implementando e/o modificando il codice di cui sopra, doveste imbattervi in un  errore GDI + generico (o altra eccezione similare) durante la gestione di file TIFF multi-pagina, vi consigliamo di dare un’occhiata a questo articolo per vedere se il problema da voi riscontrato è lo stesso di cui ci siamo occupati noi e, in caso affermativo, utilizzare il nostro workaround.

 

About Ryan

IT Project Manager, Web Interface Architect e Lead Developer di numerosi siti e servizi web ad alto traffico in Italia e in Europa. Dal 2010 si occupa anche della progettazione di App e giochi per dispositivi Android, iOS e Mobile Phone per conto di numerose società italiane. Microsoft MVP for Development Technologies dal 2018.

View all posts by Ryan

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

Questo sito usa Akismet per ridurre lo spam. Scopri come i tuoi dati vengono elaborati.