Greg Beech's Website

Scaling values in XML using XSLT

An interesting problem landed on my lap a couple of days ago: We have a UI which uses XML for layout including the sizes of the on-screen components, and need to be able to scale it up or down for zooming or different screen resolutions. I figured this could be done with a fairly simple XSLT stylesheet, although it did get a bit more complex when I found we also needed to scale the values in constructs like rect(10,30,auto,auto) and couldn't use any extension objects or inline script.

Base stylesheet

The starting point for this stylesheet is the identity transform. This creates an identical copy of the input content and can be used for such things as changing the encoding or adding/removing indenting. In this case it is a perfect base as we can use this as the standard template which will match everything and just override it for the nodes we want to scale, as the XSLT engine will choose the most specific template for any given match.

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="xml" version="1.0" encoding="utf-8" indent="yes"/>
  <xsl:template match="node() | @*">
    <xsl:copy>
      <xsl:apply-templates select="node() | @*"/>
    </xsl:copy>
  </xsl:template>
</xsl:stylesheet>

This template matches any node or attribute (note that you can use multiple predicates in a match expression by seperating them with a vertical bar), creates a copy of it, and then recursively calls itself for any child nodes or attributes. It is an incredibly useful pattern which should be in every XSLT author's toolbox.

Scaling values

To define how much the values needed to be scaled by, I added a global variable named "scaleFactor" which could be passed in. XSLT supports basic mathematical operations so this could just be used everywhere we need to perform scaling, however I created a seperate scaling template which allowed me to handle the non-numeric conditions in a central location. The template takes the value to scale as a parameter and if it is numeric then multiplies it by the scale factor, else outputs it as is. We also needed to round the values as the numbers relate to pixels on screen so the template was a convenient place to put the rounding logic.

<xsl:template name="scale-value">
  <xsl:param name="value"/>
  <xsl:choose>
    <xsl:when test="number($value)">
      <xsl:value-of select="round($value * $scaleFactor)"/>
    </xsl:when>
    <xsl:otherwise>
      <xsl:value-of select="$value"/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

As noted earlier, the XSLT engine will choose the most specific template for any given match, so to use this to scale the required attributes "height" and "width" we add a template which matches them, again using the vertical bar to allow multiple predicates:

<xsl:template match="@height | @width">
  <xsl:attribute name="{local-name()}">
    <xsl:call-template name="scale-value">
      <xsl:with-param name="value" select="."/>
    </xsl:call-template>
  </xsl:attribute>
</xsl:template>

To create the attribute with the same name as the original we use {local-name()} - in XSLT using curly brackets forces the engine to evaluate the contents, which is a built-in function to return the name of the current node. Note that if you need the { or } characters verbatim, for example embedding a GUID into the stylesheet, you can escape them by putting the bracket twice, e.g. {{ or }}.

Scaling constructs

So now the tricky bit - scaling the constructs such as rect(10,30,auto,auto). My approach to this was to create an outer template which handles the rect( and ) parts, and an inner template which recursively parses and scales the list contained within the brackets using substring functions.

The outer template takes the whole construct as a parameter named "function" (because it looks like a function call in code). The variables are used to extract the name of the function before the first bracket, and the list of values between the two brackets. Following this, the name and brackets are written to the output, calling the inner list scaling template with the values in between the brackets.

<xsl:template name="scale-function">
  <xsl:param name="function"/>
  <xsl:variable name="name" select="substring-before($function, '(')"/>
  <xsl:variable name="values" 
select="substring-before(substring-after($function, '('), ')')"/> <xsl:value-of select="$name"/> <xsl:text>(</xsl:text> <xsl:call-template name="scale-list"> <xsl:with-param name="list" select="$values"/> <xsl:with-param name="seperator" select="','"/> </xsl:call-template> <xsl:text>)</xsl:text> </xsl:template>

The inner template is the most complex part, being a recursive template to handle any number of values in the list. It takes the list as a parameter, and optionally a seperator in case we needed to handle seperators other than commas. It uses a private parameter, after, to record which part of the string it has already parsed.

The term to scale is extracted by taking the substring after the part of the string that has already been parsed, but before the next seperator. If a term is found we call the scale-value template with it, and then recurse through to the next term by adding this one onto the "after" parameter. If no term is found we assume that this is the last one (because there will be no seperator at the end of the list) so we again scale it but end the recursion.

<xsl:template name="scale-list">
  <xsl:param name="list"/>
  <xsl:param name="seperator" select="','"/>
  <xsl:param name="after"/>
  <xsl:variable name="term" 
select="substring-before(substring-after($list, $after), $seperator)"/> <xsl:choose> <xsl:when test="string-length($term) &gt; 0"> <xsl:call-template name="scale-value"> <xsl:with-param name="value" select="$term"/> </xsl:call-template> <xsl:value-of select="$seperator"/> <xsl:call-template name="scale-list"> <xsl:with-param name="list" select="$list"/> <xsl:with-param name="seperator" select="$seperator"/> <xsl:with-param name="after" select="concat($after, $term, $seperator)"/> </xsl:call-template> </xsl:when> <xsl:otherwise> <xsl:call-template name="scale-value"> <xsl:with-param name="value" select="substring-after($list, $after)"/> </xsl:call-template> </xsl:otherwise> </xsl:choose> </xsl:template>

As with the value scaling, this can easily be incorporated for the required attributes such as "shape" and "area":

<xsl:template match="@shape | @area">
  <xsl:attribute name="{local-name()}">
    <xsl:call-template name="scale-function">
      <xsl:with-param name="function" select="."/>
    </xsl:call-template>
  </xsl:attribute>
</xsl:template>

That's it! Now we have a concise stylesheet - just 80 lines - which can create an exact copy of another XML file while scaling the required values and constructs, and it took all of 30 minutes to write and test. Extending this stylesheet to allow different horizontal and vertical scale factors is left as an exercise the the reader...

The complete stylesheet

Here's the stylesheet in full, for easier viewing without the discussion:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

  <xsl:output method="xml" version="1.0" encoding="utf-8" indent="yes"/>

  <xsl:variable name="scaleFactor" select="1"/>

  <xsl:template match="node() | @*">
    <xsl:copy>
      <xsl:apply-templates select="node() | @*"/>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="@height | @width">
    <xsl:attribute name="{local-name()}">
      <xsl:call-template name="scale-value">
        <xsl:with-param name="value" select="."/>
      </xsl:call-template>
    </xsl:attribute>
  </xsl:template>

  <xsl:template match="@shape | @area">
    <xsl:attribute name="{local-name()}">
      <xsl:call-template name="scale-function">
        <xsl:with-param name="function" select="."/>
      </xsl:call-template>
    </xsl:attribute>
  </xsl:template>
  
  <xsl:template name="scale-function">
    <xsl:param name="function"/>
    <xsl:variable name="name" select="substring-before($function, '(')"/>
    <xsl:variable name="values" 
select="substring-before(substring-after($function, '('), ')')"/> <xsl:value-of select="$name"/> <xsl:text>(</xsl:text> <xsl:call-template name="scale-list"> <xsl:with-param name="list" select="$values"/> <xsl:with-param name="seperator" select="','"/> </xsl:call-template> <xsl:text>)</xsl:text> </xsl:template> <xsl:template name="scale-list"> <xsl:param name="list"/> <xsl:param name="seperator" select="','"/> <xsl:param name="after"/> <xsl:variable name="term"
select="substring-before(substring-after($list, $after), $seperator)"/> <xsl:choose> <xsl:when test="string-length($term) &gt; 0"> <xsl:call-template name="scale-value"> <xsl:with-param name="value" select="$term"/> </xsl:call-template> <xsl:value-of select="$seperator"/> <xsl:call-template name="scale-list"> <xsl:with-param name="list" select="$list"/> <xsl:with-param name="seperator" select="$seperator"/> <xsl:with-param name="after" select="concat($after, $term, $seperator)"/> </xsl:call-template> </xsl:when> <xsl:otherwise> <xsl:call-template name="scale-value"> <xsl:with-param name="value" select="substring-after($list, $after)"/> </xsl:call-template> </xsl:otherwise> </xsl:choose> </xsl:template> <xsl:template name="scale-value"> <xsl:param name="value"/> <xsl:choose> <xsl:when test="number($value)"> <xsl:value-of select="round($value * $scaleFactor)"/> </xsl:when> <xsl:otherwise> <xsl:value-of select="$value"/> </xsl:otherwise> </xsl:choose> </xsl:template> </xsl:stylesheet>

Posted Jun 27 2006, 11:04 AM by Greg Beech
Filed under:

Add a Comment

(required)  
(optional)
(required)  
Remember Me?

Enter the numbers above:
Copyright (C) Greg Beech. All rights reserved.
Powered by Community Server (Non-Commercial Edition), by Telligent Systems