From 06a595640a33158ba6ec944282f8236d965369e3 Mon Sep 17 00:00:00 2001 From: Nicolas Hiillos Date: Mon, 30 Jan 2023 17:21:45 +0200 Subject: [PATCH] Implement basic SVG primitives and 'path' skeleton --- .../drawing_controller/drawing_controller.py | 10 +- .../drawing_controller/svg_processor.py | 126 ++++++++++++++++-- 2 files changed, 123 insertions(+), 13 deletions(-) diff --git a/src/drawing_controller/drawing_controller/drawing_controller.py b/src/drawing_controller/drawing_controller/drawing_controller.py index 3bd5e03..f39b682 100644 --- a/src/drawing_controller/drawing_controller/drawing_controller.py +++ b/src/drawing_controller/drawing_controller/drawing_controller.py @@ -71,8 +71,12 @@ class DrawingController(Node): xml = ET.parse(svgpath) svg = xml.getroot() #self.map_point = map_point_function(float(svg.get('width')), float(svg.get('height')), 0.1, 0.5, -0.2, 0.2) - self.map_point = map_point_function(float(svg.get('width')), float(svg.get('height')), 0.0, 1.0, 0.0, 1.0) - self.lines = [] + try: + self.map_point = map_point_function(float(svg.get('width')), float(svg.get('height')), 0.0, 1.0, 0.0, 1.0) + except: + self.map_point = map_point_function(1000, 1000, 0.0, 1.0, 0.0, 1.0) + + self.lines = [ ((0,0),(0,0)) ] for child in svg: if (child.tag == 'line'): p1 = (float(child.get('x1')), float(child.get('y1'))) @@ -80,7 +84,7 @@ class DrawingController(Node): self.lines.append((p1,p2)) self.svg_processor = SVGProcessor(self.get_logger()) - self.svg_processor.process_svg(svgpath) + print(self.svg_processor.process_svg(svgpath)) def send_goal(self, motion): self.busy = True diff --git a/src/drawing_controller/drawing_controller/svg_processor.py b/src/drawing_controller/drawing_controller/svg_processor.py index 941feb2..d6e9d91 100644 --- a/src/drawing_controller/drawing_controller/svg_processor.py +++ b/src/drawing_controller/drawing_controller/svg_processor.py @@ -1,31 +1,136 @@ #!/usr/bin/env python3 -import rclpy import lxml.etree as ET class SVGProcessor(): + """ + Class which reads an svg file and creates sequences of points + + ... + + Attributes + ---------- + primitive_fns : dict + contains a mapping of SVG primitive names to functions that produce points + + Methods + ------- + + Services + ------- + + Topics + ------- + """ def __init__(self, logger): self.logger = logger # A dict containing svg primitive names mapping to functions that handle them - self.primitives = { + self.primitive_fns = { # Reference: # https://developer.mozilla.org/en-US/docs/Web/SVG/Element - #"line": lambda p: print("LINE"), + "line": self.primitive_line, + "polyline": self.primitive_polyline, + "polygon": self.primitive_polygon, } - def get_primitive(self, primitive): - log_error = lambda p: self.logger.error("'{}' not supported".format(p.tag)) - return self.primitives.get(primitive.tag, log_error) + def get_primitive_fn(self, primitive): + ''' + Looks up the primitive tag name in the dictionary of functions. + + Parameters: + primitive (lxml child): the primitive from the svg file + Returns: + primitive_fn (): + ''' + def log_error(p, _): + self.logger.error("'{}' not supported".format(p.tag)) + return [] + return self.primitive_fns.get(primitive.tag, log_error) + + + def down_and_up(self, points): + down = (points[0][0], points[0][1], 1) + up = (points[-1][0], points[-1][1], 1) + + return [down] + points + [up] + + def primitive_line(self, child, map_point): + p1 = map_point(float(child.get('x1')), float(child.get('y1'))) + p2 = map_point(float(child.get('x2')), float(child.get('y2'))) + return [ + (p1[0],p1[1],0), + (p2[0],p2[1],0), + ] + + def primitive_polyline(self, child, map_point): + points = child.get('points').split(' ') + points = [(map_point(float(p[0])), + map_point(float(p[1]))) + for p in points.split(',')] + output = [] + for p in points: + output.append((p[0],p[1],0)) + return output + + def primitive_polygon(self, child, map_point): + output = self.primitive_polyline(child, map_point) + output.append((output[0][0],output[0][1],0)) + return output + + # https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d + def path_parser(self, child, map_point): + path = child.get('d') + self.logger.info("Parsing path :'{}...' with {} characters".format(path[:40], len(path))) + self.logger.error("Path parser not implemented") + return [] + + # https://stackoverflow.com/questions/30232031/how-can-i-strip-namespaces-out-of-an-lxml-tree + def strip_ns_prefix(self, tree): + #xpath query for selecting all element nodes in namespace + query = "descendant-or-self::*[namespace-uri()!='']" + #for each element returned by the above xpath query... + for element in tree.xpath(query): + #replace element name with its local name + element.tag = ET.QName(element).localname + return tree def process_svg(self, svg_path): - with open(svg_path) as svg: - xml = ET.parse(svg) + with open(svg_path) as svg_file: + xml = ET.parse(svg_file) svg = xml.getroot() + svg = self.strip_ns_prefix(svg) + map_point = self.map_point_function(1000, + 1000) + if 'width' in svg.attrib: + map_point = self.map_point_function(float(svg.get('width')), + float(svg.get('height'))) + elif 'viewBox' in svg.attrib: + # TODO parse viewBox + pass + else: + self.logger.error("Unable to get SVG dimensions") + + motions = [] for child in svg: - f = self.get_primitive(child) - f(child) + self.logger.info("Attempting to process SVG primitive:'{}'".format(child.tag)) + primitive_fn = self.primitive_line + # path can consist of multiple primitives + if (child.tag == 'path'): + for m in self.path_parser(child, map_point): + motions.append(m) + else: + primitive_fn = self.get_primitive_fn(child) + motions.append(primitive_fn(child, map_point)) + + motions_refined = [] + for m in motions: + if m == []: + continue + self.logger.info("Refining:'{}'".format(m)) + motions_refined.append(self.down_and_up(m)) + return motions_refined def translate(self, val, lmin, lmax, rmin, rmax): lspan = lmax - lmin @@ -39,3 +144,4 @@ class SVGProcessor(): y = self.translate(ypix, 0, y_pixels, 0, 1) return (x,y) return map_point +