Randall's Blog

a place for my thoughts

Entries in the Category “Programming”

Using XPath Select with Genshi's Match Template

written by randall, on Oct 4, 2009 10:51:00 PM.

This entry details how I used Genshi's match templates for a two column layout with each column receiving template data. The key is XPath support and I found this page to be very helpful. Also, the genshi tutorial.

The thing to pay attention to is the syntax for "select". Both columns in the layout are selected using the XPath syntax //tag[@attribute='id_name']. So within the body that has been matched, the div with id="content" can be selected using this syntax:

    ${select("//div[@id='content']")}

The Layout Template (layout_template.html)

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:py="http://genshi.edgewall.org/" py:strip="">
<py:match path="head" once="true">
<head py:attrs="select('@*')">
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title py:with="title = list(select('title/text()'))">
    My Site - <py:if test="title">${title}</py:if>
</title>
</head>
</py:match>
<py:match path="body" once="true">
<body py:attrs="select('@*')">
	<!-- header stuff goes here -->
	<div id="page">
		<div id="content" py:strip="">
            ${select("//div[@id='content']")}
		</div>
		<!-- end #content -->

		<div id="sidebar" py:strip="">
            ${select("//div[@id='sidebar']")}
		</div>
		<!-- end #sidebar -->
		<div style="clear: both;">&nbsp;</div>
	</div>
	<!-- end #page -->
	<!-- footer stuff goes here -->
</body>
</py:match>
</html>

The Index Template (index_template.html)

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:xi="http://www.w3.org/2001/XInclude"
      xmlns:py="http://genshi.edgewall.org/"
>
<xi:include href="layout_template.html" />
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>Index Page</title>
</head>
<body>
    <div id="page">
        <div id="content">
            <p>your content</p>
	</div>
	<!-- end #content -->
        <div id="sidebar">
            <p>sidebar stuff</p>
	</div>
	<!-- end #sidebar -->
    </div>
    <!-- end #page -->
</body>
</html>

Create Reports using SVG Templates

written by randall, on Aug 6, 2009 4:37:00 PM.

A job I often encounter and have approached many ways is report creation. An oversimplified description is presenting data in a format that's useful to the recipient. When looks don't matter much, this can be as easy as creating an HTML template and merging the results of a database query, but when looks do matter the best solution isn't always apparent.

For the case when looks matter, I prefer PDF documents for the final output. The hard part is getting from raw data to PDF. Being a Python programmer I've used the reportlab library to this end with success, but often it's laborious. Also, because programming is required, non-programmers aren't able to create these reports. Many tools for end-user report creation require proprietary software and/or non-standard template formats and processes.

So what I'm looking for is:
  • template based
  • produce print ready PDF files
  • usable by non-programmers
  • use standard formats
The tools I found to meet these requirements are:
  • python (of course)
  • cairo
  • rsvg
  • Inkscape (for svg templates)

The templates are created and maintained using Inkscape in its native SVG format. In the SVG file, template fields are added as {FIELD1}, {FIELD2}, etc. A database query is run, returning fieldnames that match those in the template then python, rsvg and cairo handle template substitution and conversion to PDF.

Sample Code

def processSVGTemplate(template, data):

    filename = createNewUniqueFile()
    new_svg_file = open(filename, 'wb')

    # no need to make it difficult with xml parsing
    # just do find/replace
    svg_data = open(template).read()
    for (key, value) in data.items():
        svg_data = svg_data.replace('{%s}' % key, value)
    new_svg_file.write(svg_data)
    new_svg_file.close()

    return filename


def mergeData(template_name, data, output_path=None):
    """merge `data` with `template_path`

    returns None if `output_filename` is given or a string otherwise

    """
    template_path = os.path.join(TEMPLATE_PATH, template_name)
    # if not writing to a file, just send back a string.
    using_tempfile = False
    if output_path is None:
        using_tempfile = True
        output_path = createNewUniqueFile()
    try:
        svgt = rsvg.Handle(template_path)
        props = svgt.props
        pdf_dpi = 72.0
        width = pdf_dpi / svgt.props.dpi_x * svgt.props.width
        height = pdf_dpi / svgt.props.dpi_y * svgt.props.height
        # convert to 72 ppi
        surf = cairo.PDFSurface(output_path, width, height)
        cr = cairo.Context(surf)
        # scale the context for the change in dpi
        cr.scale(pdf_dpi / svgt.props.dpi_x, pdf_dpi / svgt.props.dpi_y)
        for rset in data:
            svg_filename = processSVGTemplate(template_path, rset)
            try:
                svg = rsvg.Handle(svg_filename)
                svg.render_cairo(cr)
                cr.show_page()
            finally:
                os.unlink(svg_filename)
        surf.finish()
        if using_tempfile:
            pdf_data = open(output_path).read()
            return pdf_data
    finally:
        if using_tempfile:
            os.unlink(output_path)

Caveats and Thoughts

  • rsvg required installing python-gnome-desktop on Debian (45Mb), which makes no sense to me.
  • I didn't do any xml parsing, but instead used string substitution. At first I used xml.dom.minidom, but it seemed needlessly complex for my needs. Maybe that will change.