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.


Best regards,

--
Joaquim Rocha
http://www.joaquimrocha.com
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.
---
 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.
+
+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.
+#
+# 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'):
+        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)
+
+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()]
+            node.attrib[attr_name] = ' '.join(values)
+        else:
+            node.attrib[attr_name] = round_if_number(attr_value)
+
+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':
+            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']
+
+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()
+    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:])
+
+
+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 * '  '
+
+    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
+
+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

------------------------------------------------------------------------------
AlienVault Unified Security Management (USM) platform delivers complete
security visibility with the essential security capabilities. Easily and
efficiently configure, manage, and operate all of your security controls
from a single console and one unified framework. Download a free trial.
http://p.sf.net/sfu/alienvault_d2d
_______________________________________________
Linuxwacom-devel mailing list
Linuxwacom-devel@lists.sourceforge.net
https://lists.sourceforge.net/lists/listinfo/linuxwacom-devel

Reply via email to