jueves, 24 de enero de 2013

Unir varios PDF con ItextSharp al tiempo que se llenan campos de formularios en los PDF

Escribo esto para ver si evito dolores de cabeza a otras personas que puedan pasar por el mismo problema que yo... si por lo menos una persona evita perder varios días buscando la solución (como me tocó a mi), entonces se librará esta entrada :D

Primero el contexto:
  1. Se requiere llenar desde C# los valores de campos en varios PDF, los campos están incrustados como formularios en el PDF.
  2. Se precisa unir los PDF resultantes para guardarlo en un solo archivo o desplegarlo en un navegador.
  3. Se está trabajando con las librerías de ITextSharp en su versión OpenSource: Community.
¿Y cuál es el problema? Que se despliegan varios errores al intentar usar las funciones de copia que vienen con las librerías si previamente se han modificado campos en alguno de los PDF. El error más común es una excepción del tipo:

System.NullReferenceException: Referencia a objeto no establecida como instancia de un objeto.
   en iTextSharp.text.pdf.PdfReader.GetNormalizedRectangle(PdfArray box) en  d:\itextsharp-all-5.3.5\itextsharp-src-core\iTextSharp\text\pdf\PdfReader.cs:línea 538

-- ó ---
System.InvalidCastException: No se puede convertir un objeto de tipo 'iTextSharp.text.pdf.PdfArray' al tipo 'iTextSharp.text.pdf.PRIndirectReference'.
   en iTextSharp.text.pdf.PdfReader.DuplicatePdfObject(PdfObject original, PdfReader newReader) en d:\itextsharp-all-5.3.5\itextsharp-src-core\iTextSharp\text\pdf\PdfReader.cs:línea 3079

=================================================================

Pues bueno, lo primero es decir que la librería en la versión 5.3.5 corrige varios de los problemas que se presentaban en la versión anterior al momento de hacer la copia... que suerte ya que esta versión es muy reciente, tanto que inicié el proyecto con la versión anterior y tuve que hacer el cambio en el camino :|

Bueno, la solución para evitar inconvenientes es sencilla (como casi siempre que uno pierde varios días buscando la solución ¬¬ ), básicamente consiste en crear un nuevo Stream que asuma el PDF con los nuevos valores de campo como si fuera el PDF original. El código es el siguiente:

[...]
                /*
                 * target es el Stream resultado. 
                 * Dependiendo de lo que se pretenda se crea uno u otro tipo de Stream. Lo demás es normal hasta el momento del cierre que se puede ver al final de este ejemplo, marcado como Primera opción o Segunda Opción*/

                // Si se va a almacenar en un archivo se utiliza un FileStream
                Stream target = new FileStream("ruta/nombre_archivo_destino.pdf", FileMode.Create);

                // Si se va a motrar en el navegador se utiliza un MemoryStream
                Stream target = new MemoryStream();

                 // Se instancian el MemoryStream que almacenará la información en cada PDF 
                // y el Documento que será el que almacene los PDF unidos
                MemoryStream ms = new MemoryStream();
                Document doc = new Document();

                // Se utiliza un PDFCopy para unir las hojas de los PDF
                // Se instancian el MemoryStream que almacenará la información en cada PDF 
                using (PdfCopy copy = new PdfCopy(doc, target)) {
                   // Importante abrir el documento para poder modificarlo
                    doc.Open();
                    copy.SetLinearPageMode();
                    PdfReader readerX = null;
                    ms = new MemoryStream();


                   // Se lee el primer archivo PDF en el cual se harán modificaciones, asignando valores a los campos
                    PdfReader r = new PdfReader("archivo1.pdf");

                   // Para poder modificar los campos se necesita trabajar con un PDFStamper
                    PdfStamper stamp = new PdfStamper(r, ms);
                    AcroFields fields = stamp.AcroFields;

                   // Se insertan los valores necesarios a los campos
                    fields.SetField("Clave_del_campo1", "Valor del campo1");

                    // Con Yes se selecciona un check
                    fields.SetField("Clave_de_un_check", "Yes");
                     //... asignación de otros campos

                  // Opcional: los siguientes valores permiten cerrar el PDF para que no se pueda modificar manualmente
                    stamp.FormFlattening = true;
                    stamp.FreeTextFlattening = true;
                    stamp.Writer.CloseStream = false;


                   // Hay que cerrar el PDFStamper para que los cambios se reflejen en el PDF
                    stamp.Close();

                   // Se cierra el reader para que el archivo no quede amarrado a la aplicación
                    r.Close();

                   // ESTE ES EL TRUCO IMPORTANTE
                   // Se crea un nuevo PDFReader con los byte[] del Stream que almacenaba el documento modificado
                    readerX = new PdfReader(ms.GetBuffer());

                  // Se copian cada una de las páginas del nuevo PDFReader
                    for (int i = 1; i <= readerX.NumberOfPages; i++) {
                        copy.AddPage(copy.GetImportedPage(readerX, i));
                    }

                   // Se lee el segundo archivo PDF
                    r = new PdfReader("archivo2.pdf");

                    // Se vuelve a cargar la memoria del Stream para el nuevo PDF
                    ms = new MemoryStream();

                   stamp = new PdfStamper(r, ms);

                   // Para mi caso no tuve que hacer modificaciones pero se puede hacer lo mismo del primer PDF
                    stamp.Close();

                   // Se cierra el reader para que el archivo no quede amarrado a la aplicación
                    r.Close();

                   // NUEVAMENTE
                   // Se crea un nuevo PDFReader con los byte[] del Stream que almacenaba el documento
                    readerX = new PdfReader(ms.GetBuffer());

                  // Se copian cada una de las páginas del nuevo PDFReader, las cuales se adicionarán al documento por debajo de las que ya se adicionaron.
                    for (int i = 1; i <= readerX.NumberOfPages; i++) {
                        copy.AddPage(copy.GetImportedPage(readerX, i));
                    }

                    // Se ejecutan un par de acciones de limpieza
                    if (readerX != null) {
                        readerX.Close();
                    }
                    ms.Dispose();

                   // Se cierra el documento para que se termine correctamente
                    if (doc != null) {
                        doc.Close();
                    }

                 }// Fin del using

                  /* En este momento ya se tiene en la variable target el Stream de todos los documentos unidos.
                  Ahora, se puede almacenar en un archivo o mostrarlo en el navegador. Lo anterior porque es una aplicación ASPX + C#, si es una aplicación de consola o WindowsForm o algo así pues se tendrán que hacer las modificaciones pertinentes al siguiente código.*/

                  // Primer opción: si se quiere almacenar en un archivo                        
                     target.Close();

                  // Segunda opción: si se quiere mostrar en el navegador
                      Response.ContentType = "application/pdf";

                        if (!target.CanRead) {
                            target = new MemoryStream(((MemoryStream)target).GetBuffer());
                        }

                        Byte[] bytes;
                        target.Seek(0, SeekOrigin.Begin);

                        bytes = new Byte[target.Length];
                        target.Read(bytes, 0, (int)target.Length);
                        Response.OutputStream.Write(bytes, 0, bytes.Length);
                        Response.End();
                        target.Close();

[...]


Bueno, eso es todo... si hay algún error en el código (que al pasarlo acá puede suceder) me lo informan por favor para corregirlo.

Gracias a la gente de ItextSharp por tan buena librería y gracias a todos los blog que dan luces para arreglar estos problemas, aunque este haya sido tan particular que no pude encontrar la solución pero si muchas ayudas parciales :( ... por eso devuelvo el favor escribiendo esta solución :D ¡ya está!.

8 comentarios:

  1. muchas gracias amigo!!!! me ayudaste de mucho, era justo lo que buscaba!!! Muchas gracias

    ResponderEliminar
  2. Que bueno saber que a alguien le sirve, me alegra haber sido de ayuda.

    Saludos

    ResponderEliminar
  3. Muchas gracias por tu codigo, me ha sido muy util. pero tengo un problema.... cuando uso Google chrome e intento descargar el PDF usando el boton de guardado propio del navegador... descarga la pagina ASPX y no el PDF. hay alguna forma de solucionarlo???

    ResponderEliminar
    Respuestas
    1. Hola Juan David...

      lo que mencionas es una característica propia del navegador que le asigna al archivo el mismo nombre de la página, no se me ocurre que cambiar a ese nivel, seguro que si le cambias la extensión al archivo descargado verás el PDF.

      Lo que se me ocurre es que puedes obligar a que el archivo no se vea en el navegador sino que siempre se descargue, en ese caso sí puedes especificar el nombre del archivo, no se si te sirva ello. Para hacerlo tendrías que colocar una línea como la siguiente:

      Response.Headers["Content-disposition"] = "attachment; filename=nombre_archivo.pdf";

      después de la línea:
      Response.ContentType = "application/pdf";

      Espero te sirva.

      Saludos

      Eliminar
  4. se puede utilizar memorystream en lugar de archivos en disco? para unirlos

    ResponderEliminar
  5. oye no sabes como puedo imprimir el resultado de una consulta

    ResponderEliminar
  6. Amigo te invito una coca-cola jajajajajaja Me has salvado del fracaso de no poder hacer esto.

    Mil gracias.

    ResponderEliminar
  7. je je... pues no tomo coca-cola pero le acepto un jugo natural o una cerveza!!! (Y)

    ResponderEliminar