--- author: '' category: '' date: 2012-03-02 02:01 description: '' link: '' priority: '' slug: BB1000 tags: '' title: Extendiendo rst2pdf y sphinx type: text updated: 2012-03-02 02:01 url_type: '' --- Sorry, spanish only post! ------------- Estimado público lector, una cosa muy especial: un post que no escribí yo! Démosle una bienvenida al autor invitado, Juan BC! ------------- Una de mis `promesas`_ en el año fue terminar uno de los proyectos mas ambiciosos que me había propuesto: terminar mi ambientación de rol utilizando el sistema matemático que había diseñado junto con un amigo tiempo atras llamado Cros_ aprovechando la barbaridad de imagenes que tenía guardada de mi tesis de grado. Como no puede ser de otra manera como miembro de Python Argentina utilice el programa utilizado por rst2pdf_ creado por `Roberto Alsina`_ el cual es el dueño de este blog En un punto necesitaba hacer una especie de plantillas de dibujos para repetir el estilo donde hay descripciones de un personaje pero el estilo se mantiene, como por ejemplo las tarjetas de este manual de `DC Universe RPG`_ : .. image:: /static/cards_dc.jpg y me dije a mi mismo *¿Por que demonios no puedo dibujar un svg_ dejar espacios en blanco* *y luego los lleno dentro del svg?* En definitiva, me imaginaba algo así: .. parsed-literal:: .. template_svg: path/a/mi/archivo/svg/que/tiene/las/variables/definidas/adentro.svg :nombre_variable_1: valor_de_variable_1 :nombre_variable_2: valor_de_variable_2 ... :nombre_variable_N: valor_de_variable_N Entonces me puse en campaña para poder programar el código python que realiza esa tarea como una extensión de rst2pdf_. **AVISO!!! este texto lo llevara a usted por cada una de las desiciones de** **diseño que involucran la creación de mi extensión para rst2pdf** Definiendo sintaxis y comportamiento ------------------------------------ Por empezar aprendí de las limitaciones de la directiva, decidí un nombre para ella y definí cual era el comportamiento que hiba a tener. * Dado que una directiva como ``template_svg`` me sonaba a larga decidí que el nombre sería ``svgt`` (SVG template). * Las directivas soportan parámetros fijos, no puedo tener un número variable de argumentos para pasarle: ``nombre_variable_1``, ``nombre_variable_2``, ``variable_N`` a algo como ``directiva(**kwargs)``. Mi solución fue pasarle todo en un json. * Por último definí que ``svgt`` generaría un nodo de tipo figure_ con lo cual mi directiva aceptaría, además de mis parámetros, los argumentos que ya tenía incorporados en dicha directiva. Esta última decisión me llevó también a contemplar que el archivo svg debería convertirse en un png, y para esto utilizaría la herramienta Inkscape_ con una llamada al sistema para realizar dicha conversión (la gran ventaja de inkscape es que puede correr totalmente headless con el parámetro ``-z``). En definitiva mi directiva tomaría un nodo asi: .. parsed-literal:: .. svgt:: file.svg :vars: { "nombre_variable_1": "valor_de_variable_1", "nombre_variable_2": "valor_de_variable_2", ..., "nombre_variable_N": "valor_de_variable_N"}
Y lo convertería en un nodo así .. parsed-literal:: .. figure:: file_con_parametros_resueltos.png
Creando el template ------------------- Yo uso para crear svg a Inkscape_ y no pensaba en ningún momento dejar de hacerlo para crear este proyecto. Así que solo era cuestión de abrir el programa y poner en la parte de donde se referencia a una url de una imagen o en un texto algún formato que indique que eso es una variable (recordemos que por dentro el svg no deja de ser texto plano). Para elegir el lenguaje de template decidí utilizar el formato que propone la clase Template_ que viene en la librería estandar de; la cual dispone que las declaraciones de hacen anteponiendo el símbolo ``$`` al nombre de la variable y pudiendo o no este nombre estar encerrado entre ``{`` y ``}``. Por ejemplo en el siguiente imagen se ve como declaro dos variables con Inkscape_ .. image:: /static/making_template.jpeg Por otra parte inkscape desde consola se ejecuta de la siguiente manera para convertir un svg a png :: $ inkscape -z file.svg -e file.png -d 300 Donde: * **inkscape** es el comando para correr inkscape. * **-z** sirve para deshabilitar el entorno gráfico. * **file.svg** es el archivo a convertir. * **-e file.png** indica que se va a exportar el archivo ``file.svg`` al formato *png* y se guardara en el archivo ``file.png``. * **-d 300** dice que el archivo ``file.png`` se creara con 300 dpi de calidad. Con esto me genero los siguientes problemas: * *¿Qué sucede si otro me pasa un template y no se cuales son las variables* *que tiene adentro?* * *¿Y si inkscape no está en el path de ejecución?* * *¿Y si no me gustan los 300 dpi de calidad?* A este punto usted lector ya se dara cuenta que todo se trata de agregarle mas variables a nuestra sintaxis ya definida: con lo cual todo este cachibache quedaría así: .. parsed-literal:: .. svgt:: file.svg :vars: { "nombre_variable_1": "valor_de_variable_1", "nombre_variable_2": "valor_de_variable_2", ..., "nombre_variable_N": "valor_de_variable_N"} :dpi: 72 :inkscape_dir: /usr/bin :list_vars_and_exit:
Siendo: * **:dpi: 72** dice que el archivo generado para la figura resultante tendra 72dpi. * **:inkscape_dir: /usr/bin** indica que el comando inkscape vive dentro de la carpeta ``/usr/bin`` * **:list_vars_and_exit:** establece que svgt solo listara por *stdout* la lista de variales existente dentro de ``file.svg`` y luego el programa terminara (notese que solo tiene motivos de debuging). En código python sólo hay que crear un string con esos huecos y luego llenarlos como en el siguiente ejemplo: .. code-block:: python import os cmd = "{isp} {svg} -z -e {png} -d {dpi}" print cmd.format(isp=os.path.join("/usr/bin/", "inkscape"), svg="file.svg", png="file.png", dpi="72") [out] /usr/bin/inkscape file.svg -z -e file.png -d 72 A los bifes ----------- Ahora si leyendo el manual de como crear `directivas para docutils`_ uno se encuentra que el esqueleto mínimo para crear estos bichos es el siguiente: .. code-block:: python # imports necesarios from docutils import nodes from docutils.parsers.rst import directives from docutils.parsers.rst import Directive from docutils.statemachine import StringList # la directiva es una clase class SVGTemplate(Directive): """ The svgt directive""" # cuantos argumentos obligatorios tenemos required_arguments = 0 # cuantos argumentos opcionales optional_arguments = 0 # si la directiva termina con una linea en blanco final_argument_whitespace = False # funciones de validación para los argumentos option_spec = {} # si puede tener mas rst adentro de la directiva has_content = True # método que hace el procesamiento def run(self): return [] # le pone nombre a la directiva y la registra directives.register_directive("svgt", SVGTemplate) Los parámetros ,,,,,,,,,,,,,, Primero empecemos definiendo un diccionario que relaciona cada nombre de parámetro (los cuales yo defino que son TODOS opcionales) con una función que los valida. Por otra parte dado la estructura que queremos generar de figure_ posee contenido y la nuestra también debe poseerlo. .. code-block:: python import json # docutils imports SPECS = { # variables mías y funciones que la validan y convierten en datos utiles # para nuestro procesamiento # convierte vars a un json "vars": json.loads, # convierte dpi a entero positivo "dpi": directives.nonnegative_int, # preprocesa el directorio quitandole espacios finales e iniciales # en blanco "inkscape_dir": directives.path, # se fija que por ser una bandera no tenga ni un valor asignado # si eso sucede retorna None sino tira un ValueError "list_vars_and_exit": directives.flag, # variables de figure "alt": directives.unchanged, "height": directives.length_or_percentage_or_unitless, "width": directives.length_or_percentage_or_unitless, "scale": directives.percentage, "align": lambda align: directives.choice(align, ('left', 'center', 'right')), "target": directives.uri, "class": directives.unchanged, "name": directives.unchanged, "figwidth": directives.length_or_percentage_or_unitless, "figclass": directives.unchanged, } # la directiva es una clase class SVGTemplate(Directive): """ The svgt directive""" required_arguments = 0 # cuantos argumentos opcionales optional_arguments = len(SPECS) final_argument_whitespace = False # funciones de validacion para los argumentos option_spec = SPECS has_content = True def run(self): return [] directives.register_directive("svgt", SVGTemplate) Todas las funciones de validación (menos ``json.load``) estan muy bien explicadas en la `documentación de docutils`_ . Entendiendo los *Nodos* ,,,,,,,,,,,,,,,,,,,,,,, La creación de nodos se hace con funciones que habitan en el modulo ``from docutils import nodes`` y todos los nodos se crean así (tomando de ejemplo figure_): .. code-block:: python my_new_node = figure(self, rawsource='', *children, **attributes) donde: * **rawsource:** es el codigo fuente del nodo que sirve para motivos de debuginng, si salta un error te muestra donde esta la falla basandose en este string. Así que mientras mas representa al nodo este argumento mejor. * ***children:** son los nodos hijos. * **\**attribute:** los atributos y opciones del nuevo nodo. si se pasa una opcion invalida simplemente se ignora. Entendienfo *figure* ,,,,,,,,,,,,,,,,,,,, Las figuras son un nodo que contiene 3 hijos: * image_ que contiene la imagen propiamente dicha. * caption_ que es un párrafo que le da una etiqueta a la imagen. * legend_ que contiene todo los parrafos sobrantes que no son el caption_ Osea algo asi: :: +---------------------------+ | figure | | | |<------ figwidth --------->| | | | +---------------------+ | | | image | | | | | | | |<--- width --------->| | | +---------------------+ | | | |The figure's caption should| |wrap at this width. | | | |Legend | +---------------------------+ Con esto en mente y con el concepto de que **TODO NODO DEBE SER PROCESADO O** **DESTRUIDO MANUALMENTE** vamos ya a encarar el algoritmo. Explicando el método ``run`` ,,,,,,,,,,,,,,,,,,,,,,,,,,,, La función run sólo tiene una condición: **debe devolver un lista que contiene nodos de docutils a ser renderizados** Así que a mano alzada run sería algo mas o menos así: .. code-block:: python def run(self): # sacamos la direccion donde se encuentra el svg uri = self.arguments[0] # extraemos nuestras variables y les asignamos un valor por defecto en # caso de no existir options = dict(self.options) svgt_vars = options.pop("vars") if "vars" in options else {} svgt_dpi = options.pop("dpi") if "dpi" in options else 72 svgt_isd = options.pop("inkscape_dir") if "inkscape_dir" in options else "" svgt_lvae = options.pop("list_vars_and_exit") == None \ if "list_vars_and_exit" in options else False # si tenemos seteado el flag list_vars_and_exit mostramos las variables # y salimos del programa if svgt_lvae: self._show_vars(uri) sys.exit(0) # por como esta diseñado figure hay que evitar que la propiedad align # le llege a su imagen interior. fig_align = options.pop("align") if "align" in options else None # pasamos a crear el archivo png png_path = self._render_svg(uri, svgt_isd, svgt_dpi, svgt_vars) # agremamos la uri del png a las opciones options["uri"] = png_path # creamos el nodo imagen image_node = nodes.image(self.block_text, **options) # el contenido de caption y legend viene todo mezclado # como un iterable llamado docutils.statemachine.StringList # hay que separarlo en dos partes para crear la figure. caption_content, legend_content = self._separate_content(self.content) # creamos el nodo caption procesando su contenido con # nested_parse caption_node = nodes.caption("\n".join(caption_content)) self.state.nested_parse(caption_content, self.content_offset, caption_node) # creamos el nodo legend y procesamos su contenido legend_node = nodes.legend("\n".join(legend_content)) self.state.nested_parse(legend_content, self.content_offset, legend_node) # restautamos la variable align para crear el nodo figure if fig_align != None: options["align"] = fig_align # creaamos el susodicho nodo figure pasandole sus hijos figure_node = nodes.figure(self.block_text, image_node, caption_node, legend_node, **options) # retornamos una lista con el nodo resultante return [figure_node] Explicando el método ``_render_svg`` ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, Este método utiliza otros dos, ``_resolve_render_name`` que sera explicado después y el método ``_call`` que debido a su extrema simplicidad se recomienda leer directamente la documentació del clase subprocess.Popen_ .. code-block:: python CMD = "{isp} {svg} -z -e {png} -d {dpi}" # donde: # - uri es la direccion donde esta el svg a renderizar # - svgt_isd es el lugar donde esta inkscape # - svgt_dpi son los dpi del png a renderizar # - svgt_vars es un diccionario que contiene los valores de las variables del svg def _render_svg(self, uri, svgt_isd, svgt_dpi, svgt_vars): # abrimos el svg y lo cargamos en un Template with open(uri) as fp: svg_tplt = string.Template(fp.read()) # reemplazamos las variables con sus valores svg_src = svg_tplt.safe_substitute(svgt_vars) # obtenemos paths donde guardar: # - El svg con las variables reemplazadas (fname_svg) # - El png resultante (fname_png) fname_svg, fname_png = self._resolve_render_name(uri) # guardamos el svg reemplazado en su lugar with open(fname_svg, "w") as fp: fp.write(svg_src) # Ponemos los parametros de ejecucion de inkscape cmd = CMD.format(isp=os.path.join(svgt_isd, "inkscape"), svg=fname_svg, png=fname_png, dpi=svgt_dpi) # ejecutamos inkscape self._call(cmd) # retornamos la direccion del nuevo png return fname_png El método ``_resolve_render_name`` ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, Este método a primera vista es inecesario, ya que podríamos generar archivos temporales eficientemente con el modulo _template, pero dado como trabaja sphinx esto no es posible de primera mano. .. code-block:: python # uri es la direccioón del svg original def _resolve_render_name(self, uri): # leemos el directorio temporal de la variable global _tempdir # que pudo haber sido modificada por sphinx o tomamos el dir temporal # del sistema operativo. Esto se debe a que sphinx corre en un sandbox # y todos los archivos deben estar en la misma carpeta donde se # encuentra el archivo de configuración conf.py tempdir = _tempdir if _tempdir != None else tempfile.gettempdir() # extraemos el nombre del archivo sin su extensión basename = os.path.basename(uri).rsplit(".", 1)[0] # agregamos al directorio temporal el nombre del archivo sin la # extensión render_name = os.path.join(tempdir, basename) # generamos un nuevo nombre para el archivo svg agregando un entero # al final para evitar coliciones idx = "" unique_svg = render_name + "{idx}" + ".svg" while os.path.exists(unique_svg.format(idx=idx)): idx = idx + 1 if isinstance(idx, int) else 1 unique_svg = unique_svg.format(idx=idx) # lo mismo para el png idx = "" unique_png = render_name + "{idx}" + ".png" while os.path.exists(unique_png.format(idx=idx)): idx = idx + 1 if isinstance(idx, int) else 1 unique_png = unique_png.format(idx=idx) # retornamos los dos valores return unique_svg, unique_png El método ``_separate_content`` ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, Este es el último método que utiliza el método ``run`` y es lo último que necesitamos para hacer el trabajo de *svgt* .. code-block:: python # recibe por parámetro un string list con todos los nodos de texto del # contenido de svgt def _separate_content(self, content): # primero creamos un nodo stringlist exclusivamente para el caption caption_cnt = StringList(parent=content.parent, parent_offset=content.parent_offset) # ahora algo IMPORTANTE.. la leyenda va a ser el mismo nodo contenido # por que la otra alternativa es crear uno nuevo y borrar el viejo # por que TODO NODO TIENE QUE SER PROCESADO O ELIMINADO MANUALMENTE legend_cnt = content # all nodes need to be procesed # si tenemos contenido copiamos el primer elemento al caption y quitamos # ese elemento de la leyenda (que es lo mismo que el content) if content: caption_cnt.append(content[0], content.source(0), content.offset(0)) content.pop(0) return caption_cnt, legend_cnt Pensando en sphinx ------------------ Para que este bicho funcione en sphinx_ es necesario agregar a nivel de modulo una función ``setup`` que recibe un único parámetro que es la instancia de sphinx_ corriendo. .. code-block:: python def setup(app): # toma el valor la referencia a la variable de módulo _tempdir global _tempdir # esta función reci be dos parámetros requeridos para usarse como # slot de la señal build-finished de sphinx y sirve para limpiar # el directorio temporal. # - la aplicación que ejecuto la señal # - la exception que genero el fin del procesamiento o None si # finalizo normalmente. def reset(app, ex): if _tempdir != None and os.path.isdir(_tempdir): shutil.rmtree(_tempdir) if ex: raise ex # agregamos una configuracion mas a sphinx llamada tempdir y asignamos # como valor por defecto _temp app.add_config_value("tempdir", "_temp", "") # tomamos el valor de tempdir de la configuración _tempdir = app.config["tempdir"] # reiniciamos el directorio temporal reset(app, None) # lo volvemos a crear os.mkdir(_tempdir) # conectamos la señal build-finished con la función reset app.connect("build-finished", reset) Si desean mas información lean la documentación del `api de extensiones de sphinx`_ Y como se usa todo esto ----------------------- Bueno con rst2pdf o bien lo tiran en la carpeta de extensiones, o en el path donde tienen su rst y luego ejecutan .. code-block:: bash $ rst2pdf archivo.rst -e svgt También pueden instalarlo desde pypi con easy_install o pip con los comandos: .. code-block:: bash $ pip install docutils_ext o .. code-block:: bash $ easy_install docutils_ext En el caso de sphinx, tienen que agregar el path donde se encuentre a sys.path y luego agregarla a la lista de extensiones. Y funciona? ----------- Pueden ver el código completo al momento de la publicación de este artículo aca_ además de poder descargar la última versión estable en la pestañita de downloads. Lo de aca abajo dice .. parsed-literal:: .. svgt:: img/temp.svg :vars: {"name": "that's all folks", "url": "img/troll.png"} .. raw:: html troll .. _figure: http://docutils.sourceforge.net/docs/ref/rst/directives.html#figure .. _image: http://docutils.sourceforge.net/docs/ref/rst/directives.html#image .. _inkscape: http://inkscape.org/ .. _promesas: http://jbcabral.com/2012/01/02/mis-promesas-para-el-2012/ .. _cros: http://jbcabral.com/publications/cros/ .. _rst2pdf: http://code.google.com/p/rst2pdf/ .. _roberto alsina: //ralsina.me/ .. _dc universe rpg: http://en.wikipedia.org/wiki/DC_Universe_Roleplaying_Game .. _template: http://docs.python.org/library/string.html?highlight=string#template-strings .. _directivas para docutils: http://docutils.sourceforge.net/docs/howto/rst-directives.html .. _documentación de docutils: http://docutils.sourceforge.net/docs/index.html#api-api-reference-material-for-client-developers .. _legend: http://docutils.sourceforge.net/docs/ref/rst/directives.html#figure .. _caption: http://docutils.sourceforge.net/docs/ref/rst/directives.html#figure .. _subprocess.popen: http://docs.python.org/library/subprocess.html?highlight=subprocess#subprocess.Popen .. _api de extensiones de sphinx: http://sphinx.pocoo.org/latest/ext/appapi.html .. _aca: https://bitbucket.org/leliel12/docutils_ext/src/18305ea1b80f/svgt.py .. _sphinx: http://sphinx.pocoo.org/