On Wed, May 22, 2013 at 06:51:01AM -0400, Joaquim Rocha wrote: > Hi, > > ----- Original Message ----- > > From: "Peter Hutterer" <peter.hutte...@who-t.net> > > To: "Joaquim Rocha" <jro...@redhat.com> > > Cc: linuxwacom-devel@lists.sourceforge.net > > Sent: Wednesday, May 22, 2013 6:41:40 AM > > Subject: Re: [Linuxwacom-devel] Script to clean new SVG layouts > > > > 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? > > What do you mean different? > > I don't think we should run it because the order of the buttons needs to > be manually change as the script doesn't, of course, know the real order > that it should be and it's often not in alphabetic order.
from the quick look I had, a few attributes were ordered differently. the data was the same. > > > 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. > > Where did you move it? sorry, that was when I applied the patch and though there were only a few changes that I fixed up locally. forgot to remove this in the final email. [...] > > > > > + > > > +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? > > Yes but you can separate values by ' ' or ',' so if you use split(','), > you'll have to split each divided string by ' ' and join them with the list. > I prefer to make the split factor uniform. ah, that makes sense then, thanks for the explanation. [...] > Check out the attached new patch, thanks. only one comment. > -- > From a0cf8b14529ccd89f32b3db32c852f3173069604 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. > > Signed-off-by: Joaquim Rocha <jro...@redhat.com> > --- > data/layouts/README | 23 +++++ > tools/clean_svg.py | 291 > ++++++++++++++++++++++++++++++++++++++++++++++++++++ > 2 files changed, 314 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..b304ad4 > --- /dev/null > +++ b/tools/clean_svg.py > @@ -0,0 +1,291 @@ > +#! /usr/bin/env python > +# > +# Copyright (c) 2013 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 + '}' > + > +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 == '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 == 'text': > + node.attrib['style'] = 'text-anchor:start;' > + elif node.tag != 'svg': > + del node.attrib['style'] > + > + remove_transform_if_exists(node) > + > + round_attrib(node, 'd', 'x', 'y', 'rx', 'ry', 'width', 'height', 'cx', > 'cy', 'r') > + > + for child in node.getchildren(): > + traverse_and_clean(child) > + > +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_and_strip_namespace(root): > + if root.tag.startswith(BRACKETS_NAMESPACE): > + root.tag = root.tag[len(BRACKETS_NAMESPACE):] > + 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_and_strip_namespace(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(',') > + try: > + a, b, c, d, e, f = [float(value.strip()) for value in values] > + except: > + return > + 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 == 'circle': > + x_attr, y_attr = 'cx', 'cy' > + elif node.tag == '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 == '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 to_string_rec(node, level=0): > + indent = '\n' + level * ' ' > + > + tag_name = node.tag > + > + # Remove 'defs' element. This cannot be done in the traverse_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 get_node_attrs_sorted(node): > + attr_value = node.attrib.get(attr) > + if attr_value is not None: > + attribs.append(indent + ' %s="%s"' % (attr, attr_value)) > + > + string = indent + '<' + tag_name + ''.join(attribs) > + if len(node) or node.text: > + string += '>' > + if not node.text or not node.text.strip(): > + node.text = indent + ' ' > + else: > + string += node.text > + if list(node): > + for child in get_node_children_sorted(node): > + string += to_string_rec(child, level+1) > + string += indent > + string += '</%s>' % tag_name > + else: > + string += ' />' > + return string > + > +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 = {'title': 0, > + 'rect': 1, > + 'circle': 2, > + 'path': 3 } > + return str(tag_order.get(arg.tag, 9)) + arg.tag > + > +def get_node_children_sorted(node): > + children = node.getchildren() > + return sorted(children, key=custom_tag_sort) > + > +def custom_attr_sort(arg): > + ''' > + Same as custom_tag_sort but for a node's attributes > + ''' > + attr_order = {'id': 0, 'class': 1, > + 'x': 2, 'y': 3, 'cx': 4, 'cy': 5, > + 'width': 6, 'height': 7} > + return str(attr_order.get(arg, 9)) + arg > + > +def get_node_attrs_sorted(node): > + attrs = node.attrib.keys() > + return sorted(attrs, key=custom_attr_sort) > + > +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 child == 'rect' or child == 'circle': shouldn't these be child.tag? with that fixed (or not-fixed if not needed) Reviewed-by: Peter Hutterer <peter.hutte...@who-t.net> feel free to push when ready Cheers, Peter > + if button_assigned: > + continue > + child.attrib['id'] = 'Button%s' % _id > + child.attrib['class'] = '%s Button' % _id > + button_assigned = True > + elif child == 'path': > + if path_assigned: > + continue > + child.attrib['id'] = 'Leader%s' % _id > + child.attrib['class'] = '%s Leader' % _id > + path_assigned = True > + elif 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_and_strip_namespace(root) > + title = root.find('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