On Thu, May 16, 2013 at 09:29:07AM -0400, Joaquim Rocha wrote:
> Producing a new SVG layout for the tablets can be a laborious task and should 
> be automated.
> 
> With that in mind I wrote a Python script to clean the SVG produced by tools 
> such as Inkscape and to automate certain things, like naming elements of 
> interest.
> No other libs/modules are needs apart from Python itself.
> 
> The script if far from perfect and needs to be tested but it has been already 
> helpful to me.
> I also updated the README in the data/layouts to show how to use it.
> 
> Hopefully you'll also find it useful and we'll include it in the next release.

yep, very useful. thanks.

I noticed that the output format is different to the current layout (well,
for the bamboo one I looked at anyway). it would make sense to run this over
all the current layouts to get a unified description - any disagreement?

> From 903c811af2532452ad8a6aed2ca479f09fd52354 Mon Sep 17 00:00:00 2001
> From: Joaquim Rocha <jro...@redhat.com>
> Date: Thu, 16 May 2013 15:21:32 +0200
> Subject: [PATCH] tools: Add clean_svg.py script
> 
> To help cleaning the SVG produced by editors such as Inkscape.
> It also automates certain things like naming the elements.

please sign-off patches to libwacom (or xf86-input-wacom, for that matter)

> ---
>  data/layouts/README |  23 +++++
>  tools/clean_svg.py  | 280 
> ++++++++++++++++++++++++++++++++++++++++++++++++++++
>  2 files changed, 303 insertions(+)
>  create mode 100755 tools/clean_svg.py
> 
> diff --git a/data/layouts/README b/data/layouts/README
> index 9da2321..ae23343 100644
> --- a/data/layouts/README
> +++ b/data/layouts/README
> @@ -227,3 +227,26 @@ Second touch-strip:
>  
>      id="LeaderStrip2Down"
>      class="Strip2Down Strip2 Leader"
> +
> +* Tips For Creating New Layouts
> +
> +Layouts use very simple SVG rules. WISIWYG editors such as Inkscape are
> +very convenient to design new layouts but usually produce a much more
> +complex SVG markup so files that are produced with those editors should
> +be cleaned. To help with this task, there is a script called clean_svg.py
> +in the tools folder.
> +Besides cleaning the markup and removing editor specific tags, clean_svg.py
> +also automates the naming of the elements.
> +
> +  * Automatic Naming with Inkscape and clean_svg.py
> +
> +  On Inkscape, be sure to group the button, leader and label elements
> +  and assign the group's ID to the desired logical name. E.g.: Assigning
> +  "A" to the group's ID and running the clean_svg.py script with that
> +  SVG, will assign "ButtonA"/"B Button" to the ID and class of the first
> +  rect/circle element found in the group; it also analogously assigns the ID 
> and class of the first path and text elements found in that group.

I've moved this onto two shorter lines.

> +
> +clean_svg.py needs two arguments, the SVG file path and the name of
> +the tablet, e.g.:
> +
> +  ./clean_svg.py /path/to/svg_file.svg "My Brand New Tablet Name"
> diff --git a/tools/clean_svg.py b/tools/clean_svg.py
> new file mode 100755
> index 0000000..0ad72c6
> --- /dev/null
> +++ b/tools/clean_svg.py
> @@ -0,0 +1,280 @@
> +#! /usr/bin/env python
> +#
> +# Copyright (c) 2011 Red Hat, Inc.

is this supposed to be 2013?

> +#
> +# This program is free software; you can redistribute it and/or modify
> +# it under the terms of the GNU General Public License as published by
> +# the Free Software Foundation; either version 2 of the License, or
> +# (at your option) any later version.
> +#
> +# This program is distributed in the hope that it will be useful,
> +# but WITHOUT ANY WARRANTY; without even the implied warranty of
> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
> +# GNU General Public License for more details.
> +#
> +# You should have received a copy of the GNU General Public License
> +# along with this program; if not, write to the Free Software
> +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
> +# Author: Joaquim Rocha <jro...@redhat.com>
> +#
> +
> +import sys
> +from argparse import ArgumentParser
> +from xml.etree import ElementTree as ET
> +
> +NAMESPACE = "http://www.w3.org/2000/svg";
> +BRACKETS_NAMESPACE = '{' + NAMESPACE + '}'
> +
> +# List with the attributes as they should be sorted
> +ATTRS_ORDER = ['id', 'class', 'x', 'y', 'cx', 'cy', 'width', 'height']
> +
> +def human_round(number):
> +    '''
> +    Round to closest .5
> +    '''
> +    return round(number * 2) / 2.0
> +
> +def traverse_and_clean(node):
> +    '''
> +    Clean the tree recursively
> +    '''
> +    # Remove any non-SVG namespace attributes
> +    for key in node.attrib.keys():
> +        if key.startswith('{'):
> +            del node.attrib[key]
> +    if node.tag == BRACKETS_NAMESPACE + 'g' and node.attrib.has_key('id'):

node_tag_is?
or better even a node_tag(foo) function that returns the non-namespaced tag so 
you can write

    if node_tag(node) == 'g':

which is more intuitive to read (imho)

or, if this is actually possible, run through the tree once and change every
node.tag to a NS-stripped tag so you don't have to worry about it. this is a
once-off script, we can afford it if it takes a bit longer.

> +        apply_id_and_class_from_group(node)
> +        del node.attrib['id']
> +    if node.attrib.has_key('style'):
> +        if node.tag == BRACKETS_NAMESPACE + 'text':
> +            node.attrib['style'] = 'text-anchor:start;'
> +        elif node.tag != BRACKETS_NAMESPACE + 'svg':
> +            del node.attrib['style']
> +
> +    remove_transform_if_exists(node)
> +
> +    round_attrib(node, 'd', 'x', 'y', 'rx', 'ry', 'width', 'height', 'cx', 
> 'cy', 'r')
> +
> +    nodes = node.getchildren()
> +    for node in nodes:
> +        traverse_and_clean(node)

this overrides the 'node' arg. no big deal, but could lead to confusing later.
for n in node.getchildren(): would be better here

> +
> +def round_attrib(node, *attrs):
> +    for attr_name in attrs:
> +        attr_value = node.attrib.get(attr_name)
> +        if attr_value is None:
> +            continue
> +        if attr_name == 'd':
> +            d = attr_value.replace(',', ' ')
> +            values = [round_if_number(value) for value in d.split()]

couldn't you just use d.split(",") here?

> +            node.attrib[attr_name] = ' '.join(values)
> +        else:
> +            node.attrib[attr_name] = round_if_number(attr_value)

    if attr_name == 'd':
        values = d.split(",")
    else:
        values = [] + [attr_value]
    node.attrib(" ".join([round_if_number(value) for value in values]))

(ok, this is really just compressing things but was a good idea in my head :)

> +
> +def round_if_number(value):
> +    try:
> +        value = human_round(float(value.strip()))
> +    except ValueError:
> +        pass
> +    return str(value)
> +
> +def remove_non_svg_nodes(root):
> +    for elem in root.getchildren():
> +        if not elem.tag.startswith(BRACKETS_NAMESPACE) or \
> +           elem.tag == BRACKETS_NAMESPACE + 'metadata':

node_tag_is (or node_tag)

> +            root.remove(elem)
> +        else:
> +            remove_non_svg_nodes(elem)
> +
> +def remove_transform_if_exists(node):
> +    TRANSLATE = 'translate'
> +    MATRIX = 'matrix'
> +
> +    transform = node.attrib.get('transform')
> +    if transform is None:
> +        return
> +    transform = transform.strip()
> +
> +    if transform.startswith(TRANSLATE):
> +        values = transform[len(TRANSLATE) + 1:-1].split(',')
> +        try:
> +            x, y = float(values[0]), float(values[1])
> +        except:
> +            return
> +
> +        apply_translation(node, 1, 0, 0, 1, x, y)
> +    elif transform.startswith(MATRIX):
> +        values = transform[len(MATRIX) + 1:-1].split(',')
> +        a, b, c, d, e, f = [float(value.strip()) for value in values]
> +        apply_translation(node, a, b, c, d, e, f)
> +        apply_scaling(node, a, d)
> +    del node.attrib['transform']

you have a try/except for TRANSLATE but not for MATRIX. is this intentional?

> +
> +def apply_translation(node, a, b, c, d, e, f):
> +    x_attr, y_attr = 'x', 'y'
> +    if node_tag_is(node, 'circle'):
> +        x_attr, y_attr = 'cx', 'cy'
> +    elif node_tag_is(node, 'path'):
> +        apply_translation_to_path(node, e, f)
> +        return
> +    try:
> +        x, y = float(node.attrib[x_attr]), float(node.attrib[y_attr])
> +        new_x = x * a + y * c + 1 * e
> +        new_y = x * b + y * d + 1 * f
> +        node.attrib[x_attr] = str(new_x)
> +        node.attrib[y_attr] = str(new_y)
> +    except:
> +        pass
> +
> +def apply_translation_to_path(node, x, y):
> +    d = node.attrib.get('d')
> +    if d is None:
> +        return
> +    d = d.replace(',', ' ').split()

split(',')

> +    m_init_index = 0
> +    length = len(d)
> +    m_end_index = length
> +    operation = 'M'
> +    i = 0
> +    while i < length:
> +        value = d[i]
> +        if value.lower() == 'm':
> +            operation = value
> +            m_init_index = i + 1
> +        elif len(value) == 1 and value.isalpha():
> +            m_end_index = i
> +            break
> +        i += 1
> +    for i in range(m_init_index, m_end_index, 2):
> +        d[i] = str(float(d[i]) + x)
> +        d[i + 1] = str(float(d[i + 1]) + y)
> +        if operation == 'm':
> +            break
> +    node.attrib['d'] = ' '.join(d[:m_init_index]) + ' ' + ' 
> '.join(d[m_init_index:m_end_index]) + ' '.join(d[m_end_index:])

this could really do with a comment on at least what type of data this handles.
looking at the first bamboo svg, data is e.g.
    M 58 100 l 20 0
and this function addds x/y to 58/100.
except that it tries to cope with anything that's not M and 2 values anyway?

having said that, it would be more intuitive to split this up into multiple
lists so you don't have to handle the init/end index.

> +
> +
> +def apply_scaling(node, x, y):
> +    w_attr, h_attr = 'width', 'height'
> +    if node_tag_is(node, 'circle'):
> +        r = float(node.attrib.get('r', 1.0))
> +        node.attrib['r'] = str(r * x)
> +    try:
> +        w = float(node.attrib[w_attr])
> +        h = float(node.attrib[h_attr])
> +        node.attrib[w_attr] = str(w * x)
> +        node.attrib[h_attr] = str(h * y)
> +    except:
> +        pass
> +
> +def node_tag_is(node, tag):
> +    return node.tag == BRACKETS_NAMESPACE + tag
> +
> +def to_string_rec(node, level=0):
> +    i = '\n' + level * '  '

please don't use 'i' here, that's for loops only

> +
> +    tag_name = node.tag[len(BRACKETS_NAMESPACE):]
> +
> +    # Remove 'defs' element. This cannot be done in the traver_and_clean
> +    # because somehow it is never found
> +    if tag_name == 'defs':
> +        return ''
> +
> +    # use a list to put id and class as the first arguments
> +    attribs = []
> +    for attr in ATTRS_ORDER:
> +        attr_value = node.attrib.get(attr)
> +        if attr_value is not None:
> +            attribs.append(i + '   %s="%s"' % (attr, attr_value))
> +            del node.attrib[attr]
> +    attribs += [i + '   %s="%s"' % (key, value) for key, value in 
> node.attrib.items()]
> +
> +    string = i + '<' + tag_name + ''.join(attribs)
> +    if len(node) or node.text:
> +        string += '>'
> +        if not node.text or not node.text.strip():
> +            node.text = i + '  '
> +        else:
> +            string += node.text
> +        if list(node):
> +            for child in get_node_children_sorted(node):
> +                string += to_string_rec(child, level+1)
> +            string += i
> +        string += '</%s>' % tag_name
> +    else:
> +        string += ' />'
> +    return string
> +
> +def get_node_children_sorted(node):
> +    '''
> +    Returns a list with the children rect nodes,
> +    the path nodes and any other nodes, in this order.
> +    '''
> +    other_nodes_index = 1
> +    children = []
> +    for child in node.getchildren():
> +        if node_tag_is(child, 'rect') or node_tag_is(child, 'circle'):
> +            children = [child] + children
> +            other_nodes_index += 1
> +        elif node_tag_is(child, 'path'):
> +            children.insert(other_nodes_index, child)
> +        else:
> +            children.append(child)
> +    return children


try this (with adjustment to get the tag into the right namespaced tag)

    def custom_tag_sort(arg):
        '''
        Use as key functon in sorted().

        Pre-fix arg tag name by a number in the sort order we want. Anything
        unspecified defaults to 9. i.e. circle → 1circle, thus sorts lower than
        other tags.
        '''
        tag_order = { 'circle' : 1,
                      'rect' : 2,
                      'path' : 3 }
        return str(tag_order.get(arg.tag, 9)) + arg.tag

    def get_node_children_sorted(node):
        return sorted(children, key=custom_tag_sort)

easier to maintain, I think. plus we're guaranteed that all other children will
be sorted too, not sure if node.getchildren() returns a sorted list by default.

could do the same for the attrib sort above.

Cheers,
   Peter

> +
> +def apply_id_and_class_from_group(group_node):
> +    button_assigned = label_assigned = path_assigned = False
> +    _id = group_node.attrib.get('id')
> +    if _id is None:
> +        return
> +    for child in group_node.getchildren():
> +        if node_tag_is(child, 'rect') or node_tag_is(child, 'circle'):
> +            if button_assigned:
> +                continue
> +            child.attrib['id'] = 'Button%s' % _id
> +            child.attrib['class'] = '%s Button' % _id
> +            button_assigned = True
> +        elif node_tag_is(child, 'path'):
> +            if path_assigned:
> +                continue
> +            child.attrib['id'] = 'Leader%s' % _id
> +            child.attrib['class'] = '%s Leader' % _id
> +            path_assigned = True
> +        elif node_tag_is(child, 'text'):
> +            if label_assigned:
> +                continue
> +            child.attrib['id'] = 'Label%s' % _id
> +            child.attrib['class'] = '%s Label' % _id
> +            child.text = _id
> +            label_assigned = True
> +
> +def to_string(root):
> +    header = '''<?xml version="1.0" standalone="no"?>
> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
> +   "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd";>'''
> +    return header + to_string_rec(root)
> +
> +def clean_svg(root, tabletname):
> +    remove_non_svg_nodes(root)
> +    title = root.find(BRACKETS_NAMESPACE + 'title')
> +    if title is not None:
> +        title.text = tabletname
> +    root.attrib['xmlns'] = 'http://www.w3.org/2000/svg'
> +    traverse_and_clean(root)
> +
> +if __name__ == '__main__':
> +    parser = ArgumentParser(description='Clean SVG files for libwacom')
> +    parser.add_argument('filename', nargs=1, type=str,
> +                        help='SVG file to clean', metavar='FILE')
> +    parser.add_argument('tabletname', nargs=1, type=str,
> +                        help='The name of the tablet', metavar='TABLET_NAME')
> +    args = parser.parse_args()
> +
> +    ET.register_namespace('', NAMESPACE)
> +    try:
> +        tree = ET.parse(args.filename[0])
> +    except Exception, e:
> +        sys.stderr.write(str(e) + '\n')
> +        exit(1)
> +    root = tree.getroot()
> +    clean_svg(root, args.tabletname[0])
> +    print to_string(root)
> -- 
> 1.8.1.4
> 


------------------------------------------------------------------------------
Try New Relic Now & We'll Send You this Cool Shirt
New Relic is the only SaaS-based application performance monitoring service 
that delivers powerful full stack analytics. Optimize and monitor your
browser, app, & servers with just a few lines of code. Try New Relic
and get this awesome Nerd Life shirt! http://p.sf.net/sfu/newrelic_d2d_may
_______________________________________________
Linuxwacom-devel mailing list
Linuxwacom-devel@lists.sourceforge.net
https://lists.sourceforge.net/lists/listinfo/linuxwacom-devel

Reply via email to