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 NuGet e SourceForge, grazie alla quale ho potuto risparmiarmi gran parte dei grattacapi.
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 |
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".
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
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!