Extendiendo rst2pdf y sphinx

So­rr­y, spa­nish on­ly pos­t!


Es­ti­ma­do pú­bli­co lec­to­r, una co­sa muy es­pe­cia­l: un post que no es­cri­bí yo! Dé­mos­le una bien­ve­ni­da al au­tor in­vi­ta­do, Juan BC!


Una de mis pro­me­sas en el año fue ter­mi­nar uno de los pro­yec­tos ma­s am­bi­cio­sos que me ha­bía pro­pues­to: ter­mi­nar mi am­bien­ta­ción de rol uti­li­zan­do el ­sis­te­ma ma­te­má­ti­co que ha­bía di­se­ña­do jun­to con un ami­go tiem­po atras lla­ma­do Cros a­pro­ve­chan­do la bar­ba­ri­dad de ima­ge­nes que te­nía guar­da­da de mi te­sis de gra­do.

Co­mo no pue­de ser de otra ma­ne­ra co­mo miem­bro de Py­thon Ar­gen­ti­na uti­li­ce el ­pro­gra­ma uti­li­za­do por rs­t2­pdf crea­do por Ro­ber­to Al­si­na el cual es el ­due­ño de es­te blog

En un pun­to ne­ce­si­ta­ba ha­cer una es­pe­cie de plan­ti­llas de di­bu­jos pa­ra re­pe­ti­r el es­ti­lo don­de hay des­crip­cio­nes de un per­so­na­je pe­ro el es­ti­lo se man­tie­ne, ­co­mo por ejem­plo las tar­je­tas de es­te ma­nual de DC Uni­ver­se RPG :

/static/cards_dc.jpg

y me di­je a mi mis­mo

¿Por que demonios no puedo dibujar un svg_ dejar espacios en blanco y luego los lleno dentro del svg?

En de­fi­ni­ti­va, me ima­gi­na­ba al­go así:

.. 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

En­ton­ces me pu­se en cam­pa­ña pa­ra po­der pro­gra­mar el có­di­go py­thon que rea­li­za e­sa ta­rea co­mo una ex­ten­sión de rs­t2­pdf.

AVI­SO­!!! es­te tex­to lo lle­va­ra a us­ted por ca­da una de las de­si­cio­nes de di­se­ño que in­vo­lu­cran la crea­ción de mi ex­ten­sión pa­ra rs­t2­pdf

Definiendo sintaxis y comportamiento

Por em­pe­zar apren­dí de las li­mi­ta­cio­nes de la di­rec­ti­va, de­ci­dí un nom­bre pa­ra ella y de­fi­ní cual era el com­por­ta­mien­to que hi­ba a te­ne­r.

  • Da­­do que una di­­re­c­­ti­­va co­­­mo te­m­­pla­­te_s­­vg me so­­­na­­ba a la­r­­ga de­­ci­­dí que el no­m­­bre se­­ría sv­­gt (S­­VG te­m­­pla­­te).

  • Las di­­re­c­­ti­­vas so­­­po­r­­tan pa­­rá­­me­­tros fi­­jo­­s, no pue­­do te­­ner un nú­­me­­ro­­ ­­va­­ria­­ble de ar­­gu­­men­­tos pa­­ra pa­sar­­le: no­m­­bre_­­va­­ria­­ble_1, no­m­­bre_­­va­­ria­­ble_2, va­­ria­­ble_N a al­­go co­­­mo di­­­re­­c­­­ti­­­va(**kwargs).

    Mi so­­­lu­­ción fue pa­sar­­le to­­­do en un jso­­n.

  • Por úl­­ti­­mo de­­fi­­ní que sv­­gt ge­­ne­­ra­­ría un no­­­do de ti­­po fi­­gu­­re con lo­­ ­­cual mi di­­re­c­­ti­­va ace­p­­ta­­ría, ade­­más de mis pa­­rá­­me­­tro­­s, los ar­­gu­­men­­tos que ­­ya te­­nía in­­co­r­­po­­­ra­­dos en di­­cha di­­re­c­­ti­­va.

    Es­­ta úl­­ti­­ma de­­ci­­sión me lle­­vó ta­m­­bién a co­n­­te­m­­plar que el ar­­chi­­vo svg de­­be­­­ría ­­co­n­­ve­r­­ti­r­­se en un pn­­g, y pa­­ra es­­to uti­­li­­za­­ría la he­­rra­­mien­­ta Inks­­ca­­pe con una lla­­ma­­da al sis­­te­­ma pa­­ra rea­­li­­zar di­­cha co­n­­ve­r­­sión (la gran ven­­ta­­ja ­­de inks­­ca­­pe es que pue­­de co­­­rrer to­­­ta­l­­men­­te hea­d­­le­ss con el pa­­rá­­me­­tro -z).

En de­fi­ni­ti­va mi di­rec­ti­va to­ma­ría un no­do asi:

.. 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"}

     <figure parameters>

Y lo con­ver­te­ría en un no­do así

.. figure:: file_con_parametros_resueltos.png
    <figure parameters>

Creando el template

Yo uso pa­ra crear svg a Inks­ca­pe y no pen­sa­ba en nin­gún mo­men­to de­jar de ha­cer­lo pa­ra crear es­te pro­yec­to. Así que so­lo era cues­tión de abrir el pro­gra­ma y po­ner en la par­te de don­de se re­fe­ren­cia a una url de una ima­gen o en un ­tex­to al­gún for­ma­to que in­di­que que eso es una va­ria­ble (re­cor­de­mos que po­r ­den­tro el svg no de­ja de ser tex­to pla­no­).

Pa­ra ele­gir el len­gua­je de tem­pla­te de­ci­dí uti­li­zar el for­ma­to que pro­po­ne ­la cla­se Tem­pla­te que vie­ne en la li­bre­ría es­tan­dar de; la cual dis­po­ne que ­las de­cla­ra­cio­nes de ha­cen ante­po­nien­do el sím­bo­lo $ al nom­bre de la ­va­ria­ble y pu­dien­do o no es­te nom­bre es­tar en­ce­rra­do en­tre { y }.

Por ejem­plo en el si­guien­te ima­gen se ve co­mo de­cla­ro dos va­ria­bles co­n Inks­ca­pe

/static/making_template.jpeg

Por otra par­te inks­ca­pe des­de con­so­la se eje­cu­ta de la si­guien­te ma­ne­ra pa­ra ­con­ver­tir un svg a png

::
$ inkscape -z file.svg -e file.png -d 300

Don­de:

  • inks­ca­pe es el co­man­do pa­ra co­rrer inks­ca­pe.
  • -z sir­ve pa­ra des­ha­bi­li­tar el en­torno grá­fi­co.
  • fi­le.s­vg es el ar­chi­vo a con­ver­ti­r.
  • -e fi­le.p­ng in­di­ca que se va a ex­por­tar el ar­chi­vo fi­le.s­vg al ­for­ma­to png y se guar­da­ra en el ar­chi­vo fi­le.p­ng.
  • -d 300 di­ce que el ar­chi­vo fi­le.p­ng se crea­ra con 300 dpi de ­ca­li­da­d.

Con es­to me ge­ne­ro los si­guien­tes pro­ble­ma­s:

  • ¿Qué su­ce­de si otro me pa­sa un tem­pla­te y no se cua­les son las va­ria­bles que tie­ne aden­tro?
  • ¿Y si inks­ca­pe no es­tá en el pa­th de eje­cu­ció­n?
  • ¿Y si no me gus­tan los 300 dpi de ca­li­da­d?

A es­te pun­to us­ted lec­tor ya se da­ra cuen­ta que to­do se tra­ta de agre­gar­le ­mas va­ria­bles a nues­tra sin­ta­xis ya de­fi­ni­da: con lo cual to­do es­te ca­chi­ba­che ­que­da­ría así:

.. 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:

     <figure parameters>

Sien­do:

  • :dpi: 72 di­ce que el ar­chi­vo ge­ne­ra­do pa­ra la fi­gu­ra re­sul­tan­te ­ten­dra 72­dpi.
  • :i­nks­ca­pe_­di­r: /us­r/­bin in­di­ca que el co­man­do inks­ca­pe vi­ve den­tro­ ­de la car­pe­ta /us­r/­bin
  • :lis­t_­var­s_an­d_e­xi­t: es­ta­ble­ce que sv­gt so­lo lis­ta­ra por stdout la ­lis­ta de va­ria­les exis­ten­te den­tro de fi­le.s­vg y lue­go el pro­gra­ma ­ter­mi­na­ra (no­te­se que so­lo tie­ne mo­ti­vos de de­bu­gin­g).

En có­di­go py­thon só­lo hay que crear un string con esos hue­cos y lue­go lle­nar­lo­s ­co­mo en el si­guien­te ejem­plo:

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

Aho­ra si le­yen­do el ma­nual de co­mo crear di­rec­ti­vas pa­ra do­cu­tils uno se en­cuen­tra que el es­que­le­to mí­ni­mo pa­ra crear es­tos bi­chos es el si­guien­te:

# 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

Pri­me­ro em­pe­ce­mos de­fi­nien­do un dic­cio­na­rio que re­la­cio­na ca­da nom­bre de ­pa­rá­me­tro (los cua­les yo de­fi­no que son TO­DOS op­cio­na­le­s) con una fun­ció­n ­que los va­li­da. Por otra par­te da­do la es­truc­tu­ra que que­re­mos ge­ne­rar de fi­gu­re po­see con­te­ni­do y la nues­tra tam­bién de­be po­seer­lo.

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)

To­das las fun­cio­nes de va­li­da­ción (me­nos jso­n.­load) es­tan muy bien­ ex­pli­ca­das en la do­cu­men­ta­ción de do­cu­tils .

Entendiendo los Nodos

La crea­ción de no­dos se ha­ce con fun­cio­nes que ha­bi­tan en el mo­du­lo from do­cu­tils im­port no­des y to­dos los no­dos se crean así (to­man­do de e­jem­plo fi­gu­re):

my_new_node = figure(self, rawsource='', *children, **attributes)

don­de:

  • raw­sour­ce: es el co­di­go fuen­te del no­do que sir­ve pa­ra mo­ti­vos de ­de­bu­ginn­g, si sal­ta un error te mues­tra don­de es­ta la fa­lla ­ba­san­do­se en es­te strin­g. Así que mien­tras mas re­pre­sen­ta al no­do es­te ar­gu­men­to me­jo­r.
  • *chil­dren: son los no­dos hi­jo­s.
  • **a­ttri­bu­te: los atri­bu­tos y op­cio­nes del nue­vo no­do. si se pa­sa u­na op­cion in­va­li­da sim­ple­men­te se ig­no­ra.

Entendienfo figure

Las fi­gu­ras son un no­do que con­tie­ne 3 hi­jo­s:

  • ima­ge que con­tie­ne la ima­gen pro­pia­men­te di­cha.
  • cap­tion que es un pá­rra­fo que le da una eti­que­ta a la ima­gen.
  • le­gend que con­tie­ne to­do los pa­rra­fos so­bran­tes que no son el cap­tion

Osea al­go asi:

+---------------------------+
|        figure             |
|                           |
|<------ figwidth --------->|
|                           |
|  +---------------------+  |
|  |     image           |  |
|  |                     |  |
|  |<--- width --------->|  |
|  +---------------------+  |
|                           |
|The figure's caption should|
|wrap at this width.        |
|                           |
|Legend                     |
+---------------------------+

Con es­to en men­te y con el con­cep­to de que TO­DO NO­DO DE­BE SER PRO­CE­SA­DO O DES­TRUI­DO MA­NUAL­MEN­TE va­mos ya a en­ca­rar el al­go­rit­mo.

Explicando el método run

La fun­ción run só­lo tie­ne una con­di­ció­n: de­be de­vol­ver un lis­ta que con­tie­ne ­no­dos de do­cu­tils a ser ren­de­ri­za­dos

Así que a ma­no al­za­da run se­ría al­go mas o me­nos así:

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

Es­te mé­to­do uti­li­za otros do­s, _re­sol­ve_­ren­de­r_­na­me que ­se­ra ex­pli­ca­do des­pués y el mé­to­do _ca­ll que de­bi­do a su ex­tre­ma sim­pli­ci­da­d ­se re­co­mien­da leer di­rec­ta­men­te la do­cu­men­ta­ció del cla­se su­bpro­ce­ss.­Po­pen

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

Es­te mé­to­do a pri­me­ra vis­ta es ine­ce­s­ario, ya que po­dría­mos ge­ne­rar ar­chi­vo­s ­tem­po­ra­les efi­cien­te­men­te con el mo­du­lo _tem­pla­te, pe­ro da­do co­mo tra­ba­ja s­phi­nx es­to no es po­si­ble de pri­me­ra ma­no.

# 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

Es­te es el úl­ti­mo mé­to­do que uti­li­za el mé­to­do run y es lo úl­ti­mo que ­ne­ce­si­ta­mos pa­ra ha­cer el tra­ba­jo de sv­gt

# 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

Pa­ra que es­te bi­cho fun­cio­ne en sphi­nx es ne­ce­s­ario agre­gar a ni­vel de mo­du­lo­ u­na fun­ción se­tup que re­ci­be un úni­co pa­rá­me­tro que es la ins­tan­cia de sphi­nx co­rrien­do.

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 de­sean mas in­for­ma­ción lean la do­cu­men­ta­ción del api de ex­ten­sio­nes de sphi­nx

Y como se usa todo esto

Bue­no con rs­t2­pdf o bien lo ti­ran en la car­pe­ta de ex­ten­sio­nes, o en el pa­th ­don­de tie­nen su rst y lue­go eje­cu­tan

$ rst2pdf archivo.rst -e svgt

Tam­bién pue­den ins­ta­lar­lo des­de py­pi con ea­s­y_ins­ta­ll o pip con los co­man­do­s:

$ pip install docutils_ext

o

$ easy_install docutils_ext

En el ca­so de sphi­n­x, tie­nen que agre­gar el pa­th don­de se en­cuen­tre a sys.­pa­th y lue­go agre­gar­la a la lis­ta de ex­ten­sio­nes.

Y funciona?

Pue­den ver el có­di­go com­ple­to al mo­men­to de la pu­bli­ca­ción de es­te ar­tí­cu­lo aca ade­más de po­der des­car­gar la úl­ti­ma ver­sión es­ta­ble en la pes­ta­ñi­ta de ­do­wn­load­s.

Lo de aca aba­jo di­ce

.. svgt:: img/temp.svg
    :vars: {"name": "that's all folks", "url": "img/troll.png"}
troll

Comentarios

Comments powered by Disqus
Contents © 2000-2013 Roberto Alsina
Share