Dagi3d v4

Generar documentos pdf con Rails y Flying Saucer

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

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