Dagi3d v4

Generar documentos pdf con Rails y Flying Saucer

Aviso: 

Este post está obsoleto. Escribí un plugin que permite generar los pdfs de manera mucho más sencilla. Más información ­aquí

---

En la aplicación que estaba haciendo en RoR necesitaba generar documentos en formato pdf a partir de los formularios rellenados por el usuario. Había comenzado a hacerlos usando la librería para ruby PDF::Writer y aparentemente funcionaba bastante bien pero resultaba un infierno el tener que maquetar toda la presentación desde código, aparte de que no me gustaba mucho la idea de hacerlo así de cara a posibles cambios de la plantilla.
De casualidad, a través de un enlace en Devzone di con un árticulo sobre la generación de pdf's en java usando las librerías Flying Saucer e iText.
Flying Saucer es una librería para renderizar documentos xhtml y css 2.1 y que ahora trabaja de manera conjunta a iText, que sirve para generar documentos pdf.
Así que la opción de tratar de integrar esta librería en la aplicación para así convertir la vista generada desde Rails en el pdf, era más que tentadora.

Para hacerlo, lo primero era descargar la librería Flyin Saucer(que ya viene con iText incluida) y generar la aplicación en Java que se encargase de convertir el documento xhtml en pdf(está copiada casi tal cual de un ejemplo del artículo):

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

import org.xhtmlrenderer.pdf.ITextRenderer;
import com.lowagie.text.DocumentException;


public class Xhtml2Pdf {

    /**
     * @param args
     */
    public static void main(String[] args) throws IOException, DocumentException {
       
        if (args.length != 2) return;
       
        String inputFile = args[0];
        String url = new File(inputFile).toURI().toURL().toString();
       
        String outputFile = args[1];
       
        OutputStream os = new FileOutputStream(outputFile);

        ITextRenderer renderer = new ITextRenderer();
        renderer.setDocument(url);
        renderer.layout();
        renderer.createPDF(os);

        os.close();
    }
}

Luego escribí una sencilla función en ruby que se encargase de ejecutar la clase hecha en java pasándole como parámetros el archivo de origen con el documento xhtml y el archivo de destino donde se generaría el pdf(lo único que tiene así de 'chicha' es que genera el classpath de manera dinámica):

def xhtml2pdf(input_file, output_file)
  
  java_dir = File.join(File.expand_path(File.dirname(__FILE__)), "java")
  jar_dir = File.join(java_dir, "jar")
  
  class_path = ".:#{java_dir}"
  
  Dir.foreach(jar_dir) do |jar|
    class_path << ":#{jar_dir}/#{jar}" if jar.match(/\.jar/)  
  end
  
  command = "java -cp #{class_path} Xhtml2Pdf #{input_file} #{output_file}"
  system(command)
end

Para poder utilizar la librería desde el controlador, tan sólo quedaba copiar el fichero en ruby dentro la carpeta lib/ del proyecto Rails, la clase en java en lib/java y los jar's necesarios para ésta, en lib/java/jar:

Y en el controlador tan sólo tenía que generar la vista y guardarla en un fichero, convertirlo en pdf y mandarlo al cliente:

require 'xhtml2pdf'

class FooController < ApplicationController
 
  def generate_pdf
        
    @document = Document.find(params[:id])
    
    xhtml = "/tmp/foo.xhtml"
    pdf = "/tmp/foo.pdf"
    
    File.open(xhtml, "w") do |file|
      file << render_to_string(:template => "pdf/document", :layout => "../pdf/pdf")
    end

    xhtml2pdf(xhtml, pdf)
    
    send_file pdf,
      :filename => "document.pdf",
      :type => "application/pdf"
  end
end

Y si quisieramos ir viendo en el navegador cómo va a quedar sin necesidad de generar el pdf, bastaría con comentar el contenido de esta función(o hacer una nueva) y generar la vista como hariamos normalmente:

render :template => "pdf/document", :layout => "../pdf/pdf"

Por último, comentar que para que Flying Saucer pueda acceder a las imágenes y hojas de estilo del documento xhtml, las rutas de estos elementos deben estar de manera absoluta apuntando a un recurso local(file://<rutadelfichero>) o bien de manera relativa a dónde se encuentre físicamente el xhtml, por lo que igual habría que hacer un helper que modificase la ruta de los recursos dependiendo de si se está previsualizando desde el navegador o se está volcando en disco.

Aunque PDF::Writer parecía funcionar bastante bien e iba más rápido ya que escribe directamente el pdf sin necesidad de parsear un documento xhtml y sus hojas de estilo, creo que en este caso compensa tener un pequeño híbrido en rails y java por el tiempo ahorrado en maquetar el pdf directamente desde ruby.

­
Jaime Iniesta
12/07/2008 06:19

Ostras! Mola un huevo! Estoy pensando usarlo para hacer la versión descargable de http://floragavarres.net

Una cosa, lo he probado en local y funciona, pero en el slicehost (debian) no tengo instalado java. ¿Qué paquetes necesito instalar para que funcione? ¿Un apt-get install java bastaría?

Jaime Iniesta
12/07/2008 06:36

Mola… lo he seguido probando en local, pero la pena es que no me pilla las imágenes y layout general (aunque le indique que si que coja el layout)… o sea, no queda tal cual la web… pero no se puede pedir todo! face-smile.png

Jaime Iniesta
12/07/2008 06:47

… o si? creo que no cogia las imágenes porque intentaba cargarlas con ruta “/images/…”… quizá si pongo el host también, lo pille. Otra cosa será el CSS… habrá que investigar porque esto promete. face-smile.png

dagi3d
14/07/2008 01:06

Como comentaba, para que me cogiese las imágenes y los css a la hora de generar el fichero, tenía que usar urls que apuntasen a un fichero local, así que hice un helper que me sacase una ruta u otra en función de si estaba haciendo una preview o ya estaba generando el fichero. Me alegra de que te resulte útil(aunque eso sí, el código es muy mejorable, que era de las primeras cosillas que hacía con Rails face-smile.png)

JuanMa
28/08/2008 02:42

Hola a todos,

Soy un poco novatillo en esto y he intentado de seguir estos pasos para poder hacer un PDF con RoR. Tengo la intención de usar este codigo para el proyecto de final de carrera, lo entrego en 2 semanas…

Al ejecutar el system(command) recibo el siguiente error:

java.lang.NoClassDefFoundError: Xhtml2Pdf

Caused by: java.lang.ClassNotFoundException: Xhtml2Pdf

at java.net.URLClassLoader$1.run(Unknown Source) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(Unknown Source) at java.lang.ClassLoader.loadClass(Unknown Source) at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source) at java.lang.ClassLoader.loadClass(Unknown Source) at java.lang.ClassLoader.loadClassInternal(Unknown Source)

Muchas gracias por vuestra ayuda.
JuanMa

Macario Ortega
17/11/2008 16:47

Gracias por la info, es de mucha ayuda.
El siguiente paso sería empacar el código en una gema.

dagi3d
18/11/2008 01:11

Me alegro que resulte útil. La verdad es que sí que sería buena idea tenerlo como gema. Cuando saque un ratillo le echo un vistazo a ver, que además seguro que el código se puede mejor.

rasilvap
24/01/2009 04:06

Mi duda principalmente es como puedo enviar mi pagina xhtml a la clase que se encarga de hacer esta conversión estoy programando con jsf arquitectura je5, y debo colocar un link de tipo h:comanLink al final de cada página la verdad no tengo la menor idea de como enviar la pagina que deseo convertir.
Por favor si alguien sabe esto agradeceria mucho posteara un ejemplo sencillo que me mostrará como hacer esto paso a paso.
Muchas gracias por su ayuda…

Flying Saucer
ocb
26/01/2009 14:49

Hola, he conseguido que me funcione, y que me cargue el css, ahora el fallo viene de la codificación, en el caso de que mi pagina a imprimir tenga un simbolo como un acento o una interrogacion me da error de utf-8, para procesar le paso un string con el contenido de la pagina. Algun comentario por favor????

kase
30/01/2009 00:35

Hola tengo un problema con ITextRenderer renderer = new ITextRenderer(); el programa corre bien hasta esta linea de codigo, pero cuando llega aca se totea, alguien sabe cual pueda ser la razon para que pase esto, o a alguien le ha pasado antes?

Deja un comentario
*: campos obligatorios. La dirección de correo no será publicada