diff --git a/nucanvas/nucanvas.py b/nucanvas/nucanvas.py
index 0474a1a..516097f 100755
--- a/nucanvas/nucanvas.py
+++ b/nucanvas/nucanvas.py
@@ -7,100 +7,99 @@
 
 
 class NuCanvas(GroupShape):
-  '''This is a clone of the Tk canvas subset used by the original Tcl
-     It implements an abstracted canvas that can render objects to different
-     backends other than just a Tk canvas widget.
-  '''
-  def __init__(self, surf):
-    GroupShape.__init__(self, surf, 0, 0, {})
-    self.markers = {}
-
-  def set_surface(self, surf):
-    self.surf = surf
-
-  def clear_shapes(self):
-    self.shapes = []
-
-  def _get_shapes(self, item=None):
-    # Filter shapes
-    if item is None or item == 'all':
-      shapes = self.shapes
-    else:
-      shapes = [s for s in self.shapes if s.is_tagged(item)]
-    return shapes
-
-  def render(self, transparent):
-    self.surf.render(self, transparent)
-
-  def add_marker(self, name, shape, ref=(0,0), orient='auto', units='stroke'):
-    self.markers[name] = (shape, ref, orient, units)
-
-  def bbox(self, item=None):
-    bx0 = 0
-    bx1 = 0
-    by0 = 0
-    by1 = 0
-
-    boxes = [s.bbox for s in self._get_shapes(item)]
-    boxes = list(zip(*boxes))
-    if len(boxes) > 0:
-      bx0 = min(boxes[0])
-      by0 = min(boxes[1])
-      bx1 = max(boxes[2])
-      by1 = max(boxes[3])
-
-    return [bx0, by0, bx1, by1]
-
-  def move(self, item, dx, dy):
-    for s in self._get_shapes(item):
-      s.move(dx, dy)
-
-  def tag_raise(self, item):
-    to_raise = self._get_shapes(item)
-    for s in to_raise:
-      self.shapes.remove(s)
-    self.shapes.extend(to_raise)
-
-  def addtag_withtag(self, tag, item):
-    for s in self._get_shapes(item):
-      s.addtag(tag)
-
-
-  def dtag(self, item, tag=None):
-    for s in self._get_shapes(item):
-      s.dtag(tag)
-
-  def draw(self, c):
-    '''Draw all shapes on the canvas'''
-    for s in self.shapes:
-      tk_draw_shape(s, c)
-
-  def delete(self, item):
-    for s in self._get_shapes(item):
-      self.shapes.remove(s)
+    '''This is a clone of the Tk canvas subset used by the original Tcl
+       It implements an abstracted canvas that can render objects to different
+       backends other than just a Tk canvas widget.
+    '''
+
+    def __init__(self, surf):
+        GroupShape.__init__(self, surf, 0, 0, {})
+        self.markers = {}
+
+    def set_surface(self, surf):
+        self.surf = surf
+
+    def clear_shapes(self):
+        self.shapes = []
+
+    def _get_shapes(self, item=None):
+        # Filter shapes
+        if item is None or item == 'all':
+            shapes = self.shapes
+        else:
+            shapes = [s for s in self.shapes if s.is_tagged(item)]
+        return shapes
+
+    def render(self, transparent):
+        self.surf.render(self, transparent)
+
+    def add_marker(self, name, shape, ref=(0, 0), orient='auto', units='stroke'):
+        self.markers[name] = (shape, ref, orient, units)
+
+    def bbox(self, item=None):
+        bx0 = 0
+        bx1 = 0
+        by0 = 0
+        by1 = 0
+
+        boxes = [s.bbox for s in self._get_shapes(item)]
+        boxes = list(zip(*boxes))
+        if len(boxes) > 0:
+            bx0 = min(boxes[0])
+            by0 = min(boxes[1])
+            bx1 = max(boxes[2])
+            by1 = max(boxes[3])
+
+        return [bx0, by0, bx1, by1]
+
+    def move(self, item, dx, dy):
+        for s in self._get_shapes(item):
+            s.move(dx, dy)
+
+    def tag_raise(self, item):
+        to_raise = self._get_shapes(item)
+        for s in to_raise:
+            self.shapes.remove(s)
+        self.shapes.extend(to_raise)
+
+    def addtag_withtag(self, tag, item):
+        for s in self._get_shapes(item):
+            s.addtag(tag)
+
+    def dtag(self, item, tag=None):
+        for s in self._get_shapes(item):
+            s.dtag(tag)
+
+    def draw(self, c):
+        '''Draw all shapes on the canvas'''
+        for s in self.shapes:
+            tk_draw_shape(s, c)
+
+    def delete(self, item):
+        for s in self._get_shapes(item):
+            self.shapes.remove(s)
 
 
 if __name__ == '__main__':
 
-  from .svg_backend import SvgSurface
-  from .cairo_backend import CairoSurface
-  from .shapes import PathShape
+    from .svg_backend import SvgSurface
+    from .cairo_backend import CairoSurface
+    from .shapes import PathShape
 
-  #surf = CairoSurface('nc.png', DrawStyle(), padding=5, scale=2)
-  surf = SvgSurface('nc.svg', DrawStyle(), padding=5, scale=2)
+    #surf = CairoSurface('nc.png', DrawStyle(), padding=5, scale=2)
+    surf = SvgSurface('nc.svg', DrawStyle(), padding=5, scale=2)
 
-  #surf.add_shape_class(DoubleRectShape, cairo_draw_DoubleRectShape)
+    #surf.add_shape_class(DoubleRectShape, cairo_draw_DoubleRectShape)
 
-  nc = NuCanvas(surf)
+    nc = NuCanvas(surf)
 
+    nc.add_marker('arrow_fwd',
+                  PathShape(((0, -4), (2, -1, 2, 1, 0, 4), (8, 0), 'z'), fill=(0, 0, 0, 120), width=0),
+                  (3.2, 0), 'auto')
 
-  nc.add_marker('arrow_fwd',
-    PathShape(((0,-4), (2,-1, 2,1, 0,4), (8,0), 'z'), fill=(0,0,0, 120), width=0),
-    (3.2,0), 'auto')
-
-  nc.add_marker('arrow_back',
-    PathShape(((0,-4), (-2,-1, -2,1, 0,4), (-8,0), 'z'), fill=(0,0,0, 120), width=0),
-    (-3.2,0), 'auto')
+    nc.add_marker('arrow_back',
+                  PathShape(((0, -4), (-2, -1, -2, 1, 0, 4), (-8, 0), 'z'), fill=(0, 0, 0, 120), width=0),
+                  (-3.2, 0), 'auto')
 
 #
 #  nc.create_rectangle(5,5, 20,20, fill=(255,0,0,127))
@@ -128,27 +127,26 @@ def delete(self, item):
 #  nc.create_path([(20,40), (30,70), (40,120, 60,50, 10), (60, 50, 80,90, 10), (80, 90, 150,89, 15),
 #       (150, 89), (130,20), 'z'], width=1)
 
-  nc.create_line(30,50, 200,100, width=5, line_color=(200,100,50,100), marker_start='arrow_back',
-    marker_end='arrow_fwd')
-
+    nc.create_line(30, 50, 200, 100, width=5, line_color=(200, 100, 50, 100), marker_start='arrow_back',
+                   marker_end='arrow_fwd')
 
-  nc.create_rectangle(30,85, 60,105, width=1, line_color=(255,0,0))
-  nc.create_line(30,90, 60,90, width=2, marker_start='arrow_back',
-    marker_end='arrow_fwd')
+    nc.create_rectangle(30, 85, 60, 105, width=1, line_color=(255, 0, 0))
+    nc.create_line(30, 90, 60, 90, width=2, marker_start='arrow_back',
+                   marker_end='arrow_fwd')
 
-  nc.create_line(30,100, 60,100, width=2, marker_start='arrow_back',
-    marker_end='arrow_fwd', marker_adjust=1.0)
+    nc.create_line(30, 100, 60, 100, width=2, marker_start='arrow_back',
+                   marker_end='arrow_fwd', marker_adjust=1.0)
 
 
 #        ls.options['marker_start'] = 'arrow_back'
 #      ls.options['marker_end'] = 'arrow_fwd'
 #      ls.options['marker_adjust'] = 0.8
 
-  nc.create_oval(50-2,80-2, 50+2,80+2, width=0, fill=(255,0,0))
-  nc.create_text(50,80, text='Hello world', anchor='nw', font=('Helvetica', 14, 'normal'), text_color=(0,0,0), spacing=-8)
+    nc.create_oval(50 - 2, 80 - 2, 50 + 2, 80 + 2, width=0, fill=(255, 0, 0))
+    nc.create_text(50, 80, text='Hello world', anchor='nw', font=('Helvetica', 14, 'normal'), text_color=(0, 0, 0), spacing=-8)
 
-  nc.create_oval(50-2,100-2, 50+2,100+2, width=0, fill=(255,0,0))
-  nc.create_text(50,100, text='Hello world', anchor='ne')
+    nc.create_oval(50 - 2, 100 - 2, 50 + 2, 100 + 2, width=0, fill=(255, 0, 0))
+    nc.create_text(50, 100, text='Hello world', anchor='ne')
 
-  surf.draw_bbox = True
-  nc.render()
+    surf.draw_bbox = True
+    nc.render(True)
diff --git a/nucanvas/shapes.py b/nucanvas/shapes.py
index e0bed91..707296b 100644
--- a/nucanvas/shapes.py
+++ b/nucanvas/shapes.py
@@ -9,603 +9,601 @@
 
 def rounded_corner(start, apex, end, rad):
 
-  # Translate all points with apex at origin
-  start = (start[0] - apex[0], start[1] - apex[1])
-  end = (end[0] - apex[0], end[1] - apex[1])
+    # Translate all points with apex at origin
+    start = (start[0] - apex[0], start[1] - apex[1])
+    end = (end[0] - apex[0], end[1] - apex[1])
 
-  # Get angles of each line segment
-  enter_a = math.atan2(start[1], start[0]) % math.radians(360)
-  leave_a = math.atan2(end[1], end[0]) % math.radians(360)
+    # Get angles of each line segment
+    enter_a = math.atan2(start[1], start[0]) % math.radians(360)
+    leave_a = math.atan2(end[1], end[0]) % math.radians(360)
 
-  #print('## enter, leave', math.degrees(enter_a), math.degrees(leave_a))
+    # print('## enter, leave', math.degrees(enter_a), math.degrees(leave_a))
 
-  # Determine bisector angle
-  ea2 = abs(enter_a - leave_a)
-  if ea2 > math.radians(180):
-    ea2 = math.radians(360) - ea2
-  bisect = ea2 / 2.0
+    # Determine bisector angle
+    ea2 = abs(enter_a - leave_a)
+    if ea2 > math.radians(180):
+        ea2 = math.radians(360) - ea2
+    bisect = ea2 / 2.0
 
-  if bisect > math.radians(82): # Nearly colinear: Skip radius
-    return (apex, apex, apex, -1)
+    if bisect > math.radians(82):  # Nearly colinear: Skip radius
+        return (apex, apex, apex, -1)
 
-  q = rad * math.sin(math.radians(90) - bisect) / math.sin(bisect)
+    q = rad * math.sin(math.radians(90) - bisect) / math.sin(bisect)
 
-  # Check that q is no more than half the shortest leg
-  enter_leg = math.sqrt(start[0]**2 + start[1]**2)
-  leave_leg = math.sqrt(end[0]**2 + end[1]**2)
-  short_leg = min(enter_leg, leave_leg)
-  if q > short_leg / 2:
-    q = short_leg / 2
-    # Compute new radius
-    rad = q * math.sin(bisect) / math.sin(math.radians(90) - bisect)
+    # Check that q is no more than half the shortest leg
+    enter_leg = math.sqrt(start[0]**2 + start[1]**2)
+    leave_leg = math.sqrt(end[0]**2 + end[1]**2)
+    short_leg = min(enter_leg, leave_leg)
+    if q > short_leg / 2:
+        q = short_leg / 2
+        # Compute new radius
+        rad = q * math.sin(bisect) / math.sin(math.radians(90) - bisect)
 
-  h = math.sqrt(q**2 + rad**2)
+    h = math.sqrt(q**2 + rad**2)
 
-  # Center of circle
+    # Center of circle
 
-  # Determine which direction is the smallest angle to the leave point
-  # Determine direction of arc
-  # Rotate whole system so that enter_a is on x-axis
-  delta = (leave_a - enter_a) % math.radians(360)
-  if delta < math.radians(180): # CW
-    bisect = enter_a + bisect
-  else: # CCW
-    bisect = enter_a - bisect
+    # Determine which direction is the smallest angle to the leave point
+    # Determine direction of arc
+    # Rotate whole system so that enter_a is on x-axis
+    delta = (leave_a - enter_a) % math.radians(360)
+    if delta < math.radians(180):  # CW
+        bisect = enter_a + bisect
+    else:  # CCW
+        bisect = enter_a - bisect
 
-  #print('## Bisect2', math.degrees(bisect))
-  center = (h * math.cos(bisect) + apex[0], h * math.sin(bisect) + apex[1])
+    # print('## Bisect2', math.degrees(bisect))
+    center = (h * math.cos(bisect) + apex[0], h * math.sin(bisect) + apex[1])
 
-  # Find start and end point of arcs
-  start_p = (q * math.cos(enter_a) + apex[0], q * math.sin(enter_a) + apex[1])
-  end_p = (q * math.cos(leave_a) + apex[0], q * math.sin(leave_a) + apex[1])
+    # Find start and end point of arcs
+    start_p = (q * math.cos(enter_a) + apex[0], q * math.sin(enter_a) + apex[1])
+    end_p = (q * math.cos(leave_a) + apex[0], q * math.sin(leave_a) + apex[1])
+
+    return (center, start_p, end_p, rad)
 
-  return (center, start_p, end_p, rad)
 
 def rotate_bbox(box, a):
-  '''Rotate a bounding box 4-tuple by an angle in degrees'''
-  corners = ( (box[0], box[1]), (box[0], box[3]), (box[2], box[3]), (box[2], box[1]) )
-  a = -math.radians(a)
-  sa = math.sin(a)
-  ca = math.cos(a)
+    '''Rotate a bounding box 4-tuple by an angle in degrees'''
+    corners = ((box[0], box[1]), (box[0], box[3]), (box[2], box[3]), (box[2], box[1]))
+    a = -math.radians(a)
+    sa = math.sin(a)
+    ca = math.cos(a)
 
-  rot = []
-  for p in corners:
-    rx = p[0]*ca + p[1]*sa
-    ry = -p[0]*sa + p[1]*ca
-    rot.append((rx,ry))
+    rot = []
+    for p in corners:
+        rx = p[0] * ca + p[1] * sa
+        ry = -p[0] * sa + p[1] * ca
+        rot.append((rx, ry))
 
-  # Find the extrema of the rotated points
-  rot = list(zip(*rot))
-  rx0 = min(rot[0])
-  rx1 = max(rot[0])
-  ry0 = min(rot[1])
-  ry1 = max(rot[1])
+    # Find the extrema of the rotated points
+    rot = list(zip(*rot))
+    rx0 = min(rot[0])
+    rx1 = max(rot[0])
+    ry0 = min(rot[1])
+    ry1 = max(rot[1])
 
-  #print('## RBB:', box, rot)
+    # print('## RBB:', box, rot)
 
-  return (rx0, ry0, rx1, ry1)
+    return (rx0, ry0, rx1, ry1)
 
 
 class BaseSurface(object):
-  def __init__(self, fname, def_styles, padding=0, scale=1.0):
-    self.fname = fname
-    self.def_styles = def_styles
-    self.padding = padding
-    self.scale = scale
-    self.draw_bbox = False
-    self.markers = {}
+    def __init__(self, fname, def_styles, padding=0, scale=1.0):
+        self.fname = fname
+        self.def_styles = def_styles
+        self.padding = padding
+        self.scale = scale
+        self.draw_bbox = False
+        self.markers = {}
 
-    self.shape_drawers = {}
+        self.shape_drawers = {}
 
-  def add_shape_class(self, sclass, drawer):
-    self.shape_drawers[sclass] = drawer
+    def add_shape_class(self, sclass, drawer):
+        self.shape_drawers[sclass] = drawer
 
-  def render(self, canvas, transparent=False):
-    pass
+    def render(self, canvas, transparent=False):
+        pass
 
-  def text_bbox(self, text, font_params, spacing):
-    pass
+    def text_bbox(self, text, font_params, spacing):
+        pass
 
 #################################
-## NuCANVAS objects
+# NuCANVAS objects
 #################################
 
 
 class DrawStyle(object):
-  def __init__(self):
-    # Set defaults
-    self.weight = 1
-    self.line_color = (0,0,255)
-    self.line_cap = 'butt'
+    def __init__(self):
+        # Set defaults
+        self.weight = 1
+        self.line_color = (0, 0, 255)
+        self.line_cap = 'butt'
 #    self.arrows = True
-    self.fill = None
-    self.text_color = (0,0,0)
-    self.font = ('Helvetica', 12, 'normal')
-    self.anchor = 'center'
-
+        self.fill = None
+        self.text_color = (0, 0, 0)
+        self.font = ('Helvetica', 12, 'normal')
+        self.anchor = 'center'
 
 
 class BaseShape(object):
-  def __init__(self, options, **kwargs):
-    self.options = {} if options is None else options
-    self.options.update(kwargs)
-
-    self._bbox = [0,0,1,1]
-    self.tags = set()
-
-  @property
-  def points(self):
-    return tuple(self._bbox)
-
-  @property
-  def bbox(self):
-    if 'weight' in self.options:
-      w = self.options['weight'] / 2.0
-    else:
-      w = 0
-
-    x0 = min(self._bbox[0], self._bbox[2])
-    x1 = max(self._bbox[0], self._bbox[2])
-    y0 = min(self._bbox[1], self._bbox[3])
-    y1 = max(self._bbox[1], self._bbox[3])
-
-    x0 -= w
-    x1 += w
-    y0 -= w
-    y1 += w
-
-    return (x0,y0,x1,y1)
-
-  @property
-  def width(self):
-    x0, _, x1, _ = self.bbox
-    return x1 - x0
-
-  @property
-  def height(self):
-    _, y0, _, y1 = self.bbox
-    return y1 - y0
-
-  @property
-  def size(self):
-    x0, y1, x1, y1 = self.bbox
-    return (x1-x0, y1-y0)
-
-
-  def param(self, name, def_styles=None):
-    if name in self.options:
-      return self.options[name]
-    elif def_styles is not None:
-      return getattr(def_styles, name)
-    else:
-      return None
-
-
-  def is_tagged(self, item):
-    return item in self.tags
-
-  def update_tags(self):
-    if 'tags' in self.options:
-      self.tags = self.tags.union(self.options['tags'])
-      del self.options['tags']
-
-  def move(self, dx, dy):
-    if self._bbox is not None:
-      self._bbox[0] += dx
-      self._bbox[1] += dy
-      self._bbox[2] += dx
-      self._bbox[3] += dy
-
-  def dtag(self, tag=None):
-    if tag is None:
-      self.tags.clear()
-    else:
-      self.tags.discard(tag)
-
-  def addtag(self, tag=None):
-    if tag is not None:
-      self.tags.add(tag)
-
-  def draw(self, c):
-    pass
-
-
-  def make_group(self):
-    '''Convert a shape into a group'''
-    parent = self.options['parent']
-
-    # Walk up the parent hierarchy until we find a GroupShape with a surface ref
-    p = parent
-    while not isinstance(p, GroupShape):
-      p = p.options['parent']
-
-    surf = p.surf
-
-    g = GroupShape(surf, 0,0, {'parent': parent})
-
-    # Add this shape as a child of the new group
-    g.shapes.append(self)
-    self.options['parent'] = g
-
-    # Replace this shape in the parent's child list
-    parent.shapes = [c if c is not self else g for c in parent.shapes]
-
-    return g
+    def __init__(self, options, **kwargs):
+        self.options = {} if options is None else options
+        self.options.update(kwargs)
+
+        self._bbox = [0, 0, 1, 1]
+        self.tags = set()
+
+    @property
+    def points(self):
+        return tuple(self._bbox)
+
+    @property
+    def bbox(self):
+        if 'weight' in self.options:
+            w = self.options['weight'] / 2.0
+        else:
+            w = 0
+
+        x0 = min(self._bbox[0], self._bbox[2])
+        x1 = max(self._bbox[0], self._bbox[2])
+        y0 = min(self._bbox[1], self._bbox[3])
+        y1 = max(self._bbox[1], self._bbox[3])
+
+        x0 -= w
+        x1 += w
+        y0 -= w
+        y1 += w
+
+        return (x0, y0, x1, y1)
+
+    @property
+    def width(self):
+        x0, _, x1, _ = self.bbox
+        return x1 - x0
+
+    @property
+    def height(self):
+        _, y0, _, y1 = self.bbox
+        return y1 - y0
+
+    @property
+    def size(self):
+        x0, y1, x1, y1 = self.bbox
+        return (x1 - x0, y1 - y0)
+
+    def param(self, name, def_styles=None):
+        if name in self.options:
+            return self.options[name]
+        elif def_styles is not None:
+            return getattr(def_styles, name)
+        else:
+            return None
+
+    def is_tagged(self, item):
+        return item in self.tags
+
+    def update_tags(self):
+        if 'tags' in self.options:
+            self.tags = self.tags.union(self.options['tags'])
+            del self.options['tags']
+
+    def move(self, dx, dy):
+        if self._bbox is not None:
+            self._bbox[0] += dx
+            self._bbox[1] += dy
+            self._bbox[2] += dx
+            self._bbox[3] += dy
+
+    def dtag(self, tag=None):
+        if tag is None:
+            self.tags.clear()
+        else:
+            self.tags.discard(tag)
+
+    def addtag(self, tag=None):
+        if tag is not None:
+            self.tags.add(tag)
+
+    def draw(self, c):
+        pass
+
+    def make_group(self):
+        '''Convert a shape into a group'''
+        parent = self.options['parent']
+
+        # Walk up the parent hierarchy until we find a GroupShape with a surface ref
+        p = parent
+        while not isinstance(p, GroupShape):
+            p = p.options['parent']
+
+        surf = p.surf
+
+        g = GroupShape(surf, 0, 0, {'parent': parent})
+
+        # Add this shape as a child of the new group
+        g.shapes.append(self)
+        self.options['parent'] = g
+
+        # Replace this shape in the parent's child list
+        parent.shapes = [c if c is not self else g for c in parent.shapes]
+
+        return g
 
 
 class GroupShape(BaseShape):
-  def __init__(self, surf, x0, y0, options, **kwargs):
-    BaseShape.__init__(self, options, **kwargs)
-    self._pos = (x0,y0)
-    self._bbox = None
-    self.shapes = []
-    self.surf = surf # Needed for TextShape to get font metrics
+    def __init__(self, surf, x0, y0, options, **kwargs):
+        BaseShape.__init__(self, options, **kwargs)
+        self._pos = (x0, y0)
+        self._bbox = None
+        self.shapes = []
+        self.surf = surf  # Needed for TextShape to get font metrics
 
 #    self.parent = None
 #    if 'parent' in options:
 #      self.parent = options['parent']
 #      del options['parent']
 
-    self.update_tags()
-
-  def ungroup(self):
-    if self.parent is None:
-      return # Can't ungroup top level canvas group
-
-    x, y = self._pos
-    for s in self.shapes:
-      s.move(x, y)
-      if isinstance(s, GroupShape):
-        s.parent = self.parent
-
-    # Transfer group children to our parent
-    pshapes = self.parent.shapes
-    pos = pshapes.index(self)
-
-    # Remove this group
-    self.parent.shapes = pshapes[:pos] + self.shapes + pshapes[pos+1:]
-
-  def ungroup_all(self):
-    for s in self.shapes:
-      if isinstance(s, GroupShape):
-        s.ungroup_all()
-    self.ungroup()
-
-  def move(self, dx, dy):
-    BaseShape.move(self, dx, dy)
-    self._pos = (self._pos[0] + dx, self._pos[1] + dy)
-
-  def create_shape(self, sclass, x0, y0, x1, y1, **options):
-    options['parent'] = self
-    shape = sclass(x0, y0, x1, y1, options)
-    self.shapes.append(shape)
-    self._bbox = None # Invalidate memoized box
-    return shape
-
-  def create_group(self, x0, y0, **options):
-    options['parent'] = self
-    shape = GroupShape(self.surf, x0, y0, options)
-    self.shapes.append(shape)
-    self._bbox = None # Invalidate memoized box
-    return shape
-
-  def create_group2(self, sclass, x0, y0, **options):
-    options['parent'] = self
-    shape = sclass(self.surf, x0, y0, options)
-    self.shapes.append(shape)
-    self._bbox = None # Invalidate memoized box
-    return shape
-
-
-  def create_arc(self, x0, y0, x1, y1, **options):
-    return self.create_shape(ArcShape, x0, y0, x1, y1, **options)
-
-  def create_line(self, x0, y0, x1, y1, **options):
-    return self.create_shape(LineShape, x0, y0, x1, y1, **options)
-
-  def create_oval(self, x0, y0, x1, y1, **options):
-    return self.create_shape(OvalShape, x0, y0, x1, y1, **options)
-
-  def create_rectangle(self, x0, y0, x1, y1, **options):
-    return self.create_shape(RectShape, x0, y0, x1, y1, **options)
-
-  def create_text(self, x0, y0, **options):
-
-    # Must set default font now so we can use its metrics to get bounding box
-    if 'font' not in options:
-      options['font'] = self.surf.def_styles.font
-
-    shape = TextShape(x0, y0, self.surf, options)
-    self.shapes.append(shape)
-    self._bbox = None # Invalidate memoized box
-
-    # Add a unique tag to serve as an ID
-    id_tag = 'id' + str(TextShape.next_text_id)
-    shape.tags.add(id_tag)
-    #return id_tag # FIXME
-    return shape
-
-  def create_path(self, nodes, **options):
-    shape = PathShape(nodes, options)
-    self.shapes.append(shape)
-    self._bbox = None # Invalidate memoized box
-    return shape
-
-
-  @property
-  def bbox(self):
-    if self._bbox is None:
-      bx0 = 0
-      bx1 = 0
-      by0 = 0
-      by1 = 0
-
-      boxes = [s.bbox for s in self.shapes]
-      boxes = list(zip(*boxes))
-      if len(boxes) > 0:
-        bx0 = min(boxes[0])
-        by0 = min(boxes[1])
-        bx1 = max(boxes[2])
-        by1 = max(boxes[3])
-
-      if 'scale' in self.options:
-        sx = sy = self.options['scale']
-        bx0 *= sx
-        by0 *= sy
-        bx1 *= sx
-        by1 *= sy
-
-      if 'angle' in self.options:
-        bx0, by0, bx1, by1 = rotate_bbox((bx0, by0, bx1, by1), self.options['angle'])
-
-      tx, ty = self._pos
-      self._bbox = [bx0+tx, by0+ty, bx1+tx, by1+ty]
-
-    return self._bbox
-
-  def dump_shapes(self, indent=0):
-    print('{}{}'.format('  '*indent, repr(self)))
-
-    indent += 1
-    for s in self.shapes:
-      if isinstance(s, GroupShape):
-        s.dump_shapes(indent)
-      else:
-        print('{}{}'.format('  '*indent, repr(s)))
+        self.update_tags()
+
+    def ungroup(self):
+        if self.parent is None:
+            return  # Can't ungroup top level canvas group
+
+        x, y = self._pos
+        for s in self.shapes:
+            s.move(x, y)
+            if isinstance(s, GroupShape):
+                s.parent = self.parent
+
+        # Transfer group children to our parent
+        pshapes = self.parent.shapes
+        pos = pshapes.index(self)
+
+        # Remove this group
+        self.parent.shapes = pshapes[:pos] + self.shapes + pshapes[pos + 1:]
+
+    def ungroup_all(self):
+        for s in self.shapes:
+            if isinstance(s, GroupShape):
+                s.ungroup_all()
+        self.ungroup()
+
+    def move(self, dx, dy):
+        BaseShape.move(self, dx, dy)
+        self._pos = (self._pos[0] + dx, self._pos[1] + dy)
+
+    def create_shape(self, sclass, x0, y0, x1, y1, **options):
+        options['parent'] = self
+        shape = sclass(x0, y0, x1, y1, options)
+        self.shapes.append(shape)
+        self._bbox = None  # Invalidate memoized box
+        return shape
+
+    def create_group(self, x0, y0, **options):
+        options['parent'] = self
+        shape = GroupShape(self.surf, x0, y0, options)
+        self.shapes.append(shape)
+        self._bbox = None  # Invalidate memoized box
+        return shape
+
+    def create_group2(self, sclass, x0, y0, **options):
+        options['parent'] = self
+        shape = sclass(self.surf, x0, y0, options)
+        self.shapes.append(shape)
+        self._bbox = None  # Invalidate memoized box
+        return shape
+
+    def create_arc(self, x0, y0, x1, y1, **options):
+        return self.create_shape(ArcShape, x0, y0, x1, y1, **options)
+
+    def create_line(self, x0, y0, x1, y1, **options):
+        return self.create_shape(LineShape, x0, y0, x1, y1, **options)
+
+    def create_oval(self, x0, y0, x1, y1, **options):
+        return self.create_shape(OvalShape, x0, y0, x1, y1, **options)
+
+    def create_rectangle(self, x0, y0, x1, y1, **options):
+        return self.create_shape(RectShape, x0, y0, x1, y1, **options)
+
+    def create_text(self, x0, y0, **options):
+
+        # Must set default font now so we can use its metrics to get bounding box
+        if 'font' not in options:
+            options['font'] = self.surf.def_styles.font
+
+        shape = TextShape(x0, y0, self.surf, options)
+        self.shapes.append(shape)
+        self._bbox = None  # Invalidate memoized box
+
+        # Add a unique tag to serve as an ID
+        id_tag = 'id' + str(TextShape.next_text_id)
+        shape.tags.add(id_tag)
+        # return id_tag # FIXME
+        return shape
+
+    def create_path(self, nodes, **options):
+        shape = PathShape(nodes, options)
+        self.shapes.append(shape)
+        self._bbox = None  # Invalidate memoized box
+        return shape
+
+    @property
+    def bbox(self):
+        if self._bbox is None:
+            bx0 = 0
+            bx1 = 0
+            by0 = 0
+            by1 = 0
+
+            boxes = [s.bbox for s in self.shapes]
+            boxes = list(zip(*boxes))
+            if len(boxes) > 0:
+                bx0 = min(boxes[0])
+                by0 = min(boxes[1])
+                bx1 = max(boxes[2])
+                by1 = max(boxes[3])
+
+            if 'scale' in self.options:
+                sx = sy = self.options['scale']
+                bx0 *= sx
+                by0 *= sy
+                bx1 *= sx
+                by1 *= sy
+
+            if 'angle' in self.options:
+                bx0, by0, bx1, by1 = rotate_bbox((bx0, by0, bx1, by1), self.options['angle'])
+
+            tx, ty = self._pos
+            self._bbox = [bx0 + tx, by0 + ty, bx1 + tx, by1 + ty]
+
+        return self._bbox
+
+    def dump_shapes(self, indent=0):
+        print('{}{}'.format('  ' * indent, repr(self)))
+
+        indent += 1
+        for s in self.shapes:
+            if isinstance(s, GroupShape):
+                s.dump_shapes(indent)
+            else:
+                print('{}{}'.format('  ' * indent, repr(s)))
+
 
 class LineShape(BaseShape):
-  def __init__(self, x0, y0, x1, y1, options=None, **kwargs):
-    BaseShape.__init__(self, options, **kwargs)
-    self._bbox = [x0, y0, x1, y1]
-    self.update_tags()
+    def __init__(self, x0, y0, x1, y1, options=None, **kwargs):
+        BaseShape.__init__(self, options, **kwargs)
+        self._bbox = [x0, y0, x1, y1]
+        self.update_tags()
+
 
 class RectShape(BaseShape):
-  def __init__(self, x0, y0, x1, y1, options=None, **kwargs):
-    BaseShape.__init__(self, options, **kwargs)
-    self._bbox = [x0, y0, x1, y1]
-    self.update_tags()
+    def __init__(self, x0, y0, x1, y1, options=None, **kwargs):
+        BaseShape.__init__(self, options, **kwargs)
+        self._bbox = [x0, y0, x1, y1]
+        self.update_tags()
 
 
 class OvalShape(BaseShape):
-  def __init__(self, x0, y0, x1, y1, options=None, **kwargs):
-    BaseShape.__init__(self, options, **kwargs)
-    self._bbox = [x0, y0, x1, y1]
-    self.update_tags()
+    def __init__(self, x0, y0, x1, y1, options=None, **kwargs):
+        BaseShape.__init__(self, options, **kwargs)
+        self._bbox = [x0, y0, x1, y1]
+        self.update_tags()
+
 
 class ArcShape(BaseShape):
-  def __init__(self, x0, y0, x1, y1, options=None, **kwargs):
-    if 'closed' not in options:
-      options['closed'] = False
+    def __init__(self, x0, y0, x1, y1, options=None, **kwargs):
+        if 'closed' not in options:
+            options['closed'] = False
 
-    BaseShape.__init__(self, options, **kwargs)
-    self._bbox = [x0, y0, x1, y1]
-    self.update_tags()
+        BaseShape.__init__(self, options, **kwargs)
+        self._bbox = [x0, y0, x1, y1]
+        self.update_tags()
 
-  @property
-  def bbox(self):
-    lw = self.param('weight')
-    if lw is None:
-      lw = 0
+    @property
+    def bbox(self):
+        lw = self.param('weight')
+        if lw is None:
+            lw = 0
 
-    lw /= 2.0
+        lw /= 2.0
 
-    # Calculate bounding box for arc segment
-    x0, y0, x1, y1 = self.points
-    xc = (x0 + x1) / 2.0
-    yc = (y0 + y1) / 2.0
-    hw = abs(x1 - x0) / 2.0
-    hh = abs(y1 - y0) / 2.0
+        # Calculate bounding box for arc segment
+        x0, y0, x1, y1 = self.points
+        xc = (x0 + x1) / 2.0
+        yc = (y0 + y1) / 2.0
+        hw = abs(x1 - x0) / 2.0
+        hh = abs(y1 - y0) / 2.0
 
-    start = self.options['start'] % 360
-    extent = self.options['extent']
-    stop = (start + extent) % 360
+        start = self.options['start'] % 360
+        extent = self.options['extent']
+        stop = (start + extent) % 360
 
-    if extent < 0:
-      start, stop = stop, start  # Swap points so we can rotate CCW
+        if extent < 0:
+            start, stop = stop, start  # Swap points so we can rotate CCW
 
-    if stop < start:
-      stop += 360 # Make stop greater than start
+        if stop < start:
+            stop += 360  # Make stop greater than start
 
-    angles = [start, stop]
+        angles = [start, stop]
 
-    # Find the extrema of the circle included in the arc
-    ortho = (start // 90) * 90 + 90
-    while ortho < stop:
-      angles.append(ortho)
-      ortho += 90 # Rotate CCW
+        # Find the extrema of the circle included in the arc
+        ortho = (start // 90) * 90 + 90
+        while ortho < stop:
+            angles.append(ortho)
+            ortho += 90  # Rotate CCW
 
+        # Convert all extrema points to cartesian
+        points = [(hw * math.cos(math.radians(a)), -hh * math.sin(math.radians(a))) for a in angles]
 
-    # Convert all extrema points to cartesian
-    points = [(hw * math.cos(math.radians(a)), -hh * math.sin(math.radians(a))) for a in angles]
+        points = list(zip(*points))
+        x0 = min(points[0]) + xc - lw
+        y0 = min(points[1]) + yc - lw
+        x1 = max(points[0]) + xc + lw
+        y1 = max(points[1]) + yc + lw
 
-    points = list(zip(*points))
-    x0 = min(points[0]) + xc - lw
-    y0 = min(points[1]) + yc - lw
-    x1 = max(points[0]) + xc + lw
-    y1 = max(points[1]) + yc + lw
+        if 'weight' in self.options:
+            w = self.options['weight'] / 2.0
+            # FIXME: This doesn't properly compensate for the true extrema of the stroked outline
+            x0 -= w
+            x1 += w
+            y0 -= w
+            y1 += w
 
-    if 'weight' in self.options:
-      w = self.options['weight'] / 2.0
-      # FIXME: This doesn't properly compensate for the true extrema of the stroked outline
-      x0 -= w
-      x1 += w
-      y0 -= w
-      y1 += w
+        #print('@@ ARC BB:', (bx0,by0,bx1,by1), hw, hh, angles, start, extent)
+        return (x0, y0, x1, y1)
 
-    #print('@@ ARC BB:', (bx0,by0,bx1,by1), hw, hh, angles, start, extent)
-    return (x0,y0,x1,y1)
 
 class PathShape(BaseShape):
-  def __init__(self, nodes, options=None, **kwargs):
-    BaseShape.__init__(self, options, **kwargs)
-    self.nodes = nodes
-    self.update_tags()
-
-  @property
-  def bbox(self):
-    extrema = []
-    for p in self.nodes:
-      if len(p) == 2:
-        extrema.append(p)
-      elif len(p) == 6: # FIXME: Compute tighter extrema of spline
-        extrema.append(p[0:2])
-        extrema.append(p[2:4])
-        extrema.append(p[4:6])
-      elif len(p) == 5: # Arc
-        extrema.append(p[0:2])
-        extrema.append(p[2:4])
-
-    extrema = list(zip(*extrema))
-    x0 = min(extrema[0])
-    y0 = min(extrema[1])
-    x1 = max(extrema[0])
-    y1 = max(extrema[1])
-
-    if 'weight' in self.options:
-      w = self.options['weight'] / 2.0
-      # FIXME: This doesn't properly compensate for the true extrema of the stroked outline
-      x0 -= w
-      x1 += w
-      y0 -= w
-      y1 += w
-
-    return (x0, y0, x1, y1)
-
+    def __init__(self, nodes, options=None, **kwargs):
+        BaseShape.__init__(self, options, **kwargs)
+        self.nodes = nodes
+        self.update_tags()
+
+    @property
+    def bbox(self):
+        extrema = []
+        for p in self.nodes:
+            if len(p) == 2:
+                extrema.append(p)
+            elif len(p) == 6:  # FIXME: Compute tighter extrema of spline
+                extrema.append(p[0:2])
+                extrema.append(p[2:4])
+                extrema.append(p[4:6])
+            elif len(p) == 5:  # Arc
+                extrema.append(p[0:2])
+                extrema.append(p[2:4])
+
+        extrema = list(zip(*extrema))
+        x0 = min(extrema[0])
+        y0 = min(extrema[1])
+        x1 = max(extrema[0])
+        y1 = max(extrema[1])
+
+        if 'weight' in self.options:
+            w = self.options['weight'] / 2.0
+            # FIXME: This doesn't properly compensate for the true extrema of the stroked outline
+            x0 -= w
+            x1 += w
+            y0 -= w
+            y1 += w
+
+        return (x0, y0, x1, y1)
 
 
 class TextShape(BaseShape):
-  text_id = 1
-  def __init__(self, x0, y0, surf, options=None, **kwargs):
-    BaseShape.__init__(self, options, **kwargs)
-    self._pos = (x0, y0)
+    text_id = 1
 
-    if 'spacing' not in options:
-      options['spacing'] = -8
-    if 'anchor' not in options:
-      options['anchor'] = 'c'
+    def __init__(self, x0, y0, surf, options=None, **kwargs):
+        BaseShape.__init__(self, options, **kwargs)
+        self._pos = (x0, y0)
 
-    spacing = options['spacing']
+        if 'spacing' not in options:
+            options['spacing'] = -8
+        if 'anchor' not in options:
+            options['anchor'] = 'c'
 
-    bx0,by0, bx1,by1, baseline = surf.text_bbox(options['text'], options['font'], spacing)
-    w = bx1 - bx0
-    h = by1 - by0
+        spacing = options['spacing']
 
-    self._baseline = baseline
-    self._bbox = [x0, y0, x0+w, y0+h]
-    self._anchor_off = self.anchor_offset
+        bx0, by0, bx1, by1, baseline = surf.text_bbox(options['text'], options['font'], spacing)
+        w = bx1 - bx0
+        h = by1 - by0
 
-    self.update_tags()
+        self._baseline = baseline
+        self._bbox = [x0, y0, x0 + w, y0 + h]
+        self._anchor_off = self.anchor_offset
 
-  @property
-  def bbox(self):
-    x0, y0, x1, y1 = self._bbox
-    ax, ay = self._anchor_off
-    return (x0+ax, y0+ay, x1+ax, y1+ay)
+        self.update_tags()
 
-  @property
-  def anchor_decode(self):
-    anchor = self.param('anchor').lower()
+    @property
+    def bbox(self):
+        x0, y0, x1, y1 = self._bbox
+        ax, ay = self._anchor_off
+        return (x0 + ax, y0 + ay, x1 + ax, y1 + ay)
 
-    anchor = anchor.replace('center','c')
-    anchor = anchor.replace('east','e')
-    anchor = anchor.replace('west','w')
+    @property
+    def anchor_decode(self):
+        anchor = self.param('anchor').lower()
 
-    if 'e' in anchor:
-      anchorh = 'e'
-    elif 'w' in anchor:
-      anchorh = 'w'
-    else:
-      anchorh = 'c'
+        anchor = anchor.replace('center', 'c')
+        anchor = anchor.replace('east', 'e')
+        anchor = anchor.replace('west', 'w')
 
-    if 'n' in anchor:
-      anchorv = 'n'
-    elif 's' in anchor:
-      anchorv = 's'
-    else:
-      anchorv = 'c'
+        if 'e' in anchor:
+            anchorh = 'e'
+        elif 'w' in anchor:
+            anchorh = 'w'
+        else:
+            anchorh = 'c'
 
-    return (anchorh, anchorv)
+        if 'n' in anchor:
+            anchorv = 'n'
+        elif 's' in anchor:
+            anchorv = 's'
+        else:
+            anchorv = 'c'
 
-  @property
-  def anchor_offset(self):
-    x0, y0, x1, y1 = self._bbox
-    w = abs(x1 - x0)
-    h = abs(y1 - y0)
-    hw = w / 2.0
-    hh = h / 2.0
+        return (anchorh, anchorv)
 
-    spacing = self.param('spacing')
+    @property
+    def anchor_offset(self):
+        x0, y0, x1, y1 = self._bbox
+        w = abs(x1 - x0)
+        h = abs(y1 - y0)
+        hw = w / 2.0
+        hh = h / 2.0
 
-    anchorh, anchorv = self.anchor_decode
-    ax = 0
-    ay = 0
+        spacing = self.param('spacing')
 
-    if 'n' in anchorv:
-      ay = hh + (spacing // 2)
-    elif 's' in anchorv:
-      ay = -hh - (spacing // 2)
+        anchorh, anchorv = self.anchor_decode
+        ax = 0
+        ay = 0
 
-    if 'e' in anchorh:
-      ax = -hw
-    elif 'w' in anchorh:
-      ax = hw
+        if 'n' in anchorv:
+            ay = hh + (spacing // 2)
+        elif 's' in anchorv:
+            ay = -hh - (spacing // 2)
 
-    # Convert from center to upper-left corner
-    return (ax - hw, ay - hh)
+        if 'e' in anchorh:
+            ax = -hw
+        elif 'w' in anchorh:
+            ax = hw
 
-  @property
-  def next_text_id(self):
-    rval = TextShape.text_id
-    TextShape.text_id += 1
-    return rval
+        # Convert from center to upper-left corner
+        return (ax - hw, ay - hh)
 
+    @property
+    def next_text_id(self):
+        rval = TextShape.text_id
+        TextShape.text_id += 1
+        return rval
 
 
 class DoubleRectShape(BaseShape):
-  def __init__(self, x0, y0, x1, y1, options=None, **kwargs):
-    BaseShape.__init__(self, options, **kwargs)
-    self._bbox = [x0, y0, x1, y1]
-    self.update_tags()
+    def __init__(self, x0, y0, x1, y1, options=None, **kwargs):
+        BaseShape.__init__(self, options, **kwargs)
+        self._bbox = [x0, y0, x1, y1]
+        self.update_tags()
+
 
 def cairo_draw_DoubleRectShape(shape, surf):
-  c = surf.ctx
-  x0, y0, x1, y1 = shape.points
+    c = surf.ctx
+    x0, y0, x1, y1 = shape.points
 
-  c.rectangle(x0,y0, x1-x0,y1-y0)
+    c.rectangle(x0, y0, x1 - x0, y1 - y0)
 
-  stroke = True if shape.options['weight'] > 0 else False
+    stroke = True if shape.options['weight'] > 0 else False
 
-  if 'fill' in shape.options:
-    c.set_source_rgba(*rgb_to_cairo(shape.options['fill']))
-    if stroke:
-      c.fill_preserve()
-    else:
-      c.fill()
+    if 'fill' in shape.options:
+        c.set_source_rgba(*rgb_to_cairo(shape.options['fill']))
+        if stroke:
+            c.fill_preserve()
+        else:
+            c.fill()
 
-  if stroke:
-    # FIXME c.set_source_rgba(*default_pen)
-    c.set_source_rgba(*rgb_to_cairo((100,200,100)))
-    c.stroke()
+    if stroke:
+        # FIXME c.set_source_rgba(*default_pen)
+        c.set_source_rgba(*rgb_to_cairo((100, 200, 100)))
+        c.stroke()
 
-    c.rectangle(x0+4,y0+4, x1-x0-8,y1-y0-8)
-    c.stroke()
+        c.rectangle(x0 + 4, y0 + 4, x1 - x0 - 8, y1 - y0 - 8)
+        c.stroke()
diff --git a/nucanvas/svg_backend.py b/nucanvas/svg_backend.py
index b32f962..3844513 100644
--- a/nucanvas/svg_backend.py
+++ b/nucanvas/svg_backend.py
@@ -13,23 +13,25 @@
 from .cairo_backend import CairoSurface
 
 #################################
-## SVG objects
+# SVG objects
 #################################
 
+
 def cairo_font(tk_font):
-  family, size, weight = tk_font
-  return pango.FontDescription('{} {} {}'.format(family, weight, size))
+    family, size, weight = tk_font
+    return pango.FontDescription('{} {} {}'.format(family, weight, size))
 
 
 def rgb_to_hex(rgb):
-  return '#{:02X}{:02X}{:02X}'.format(*rgb[:3])
+    return '#{:02X}{:02X}{:02X}'.format(*rgb[:3])
+
 
 def hex_to_rgb(hex_color):
-  v = int(hex_color[1:], 16)
-  b = v & 0xFF
-  g = (v >> 8) & 0xFF
-  r = (v >> 16) & 0xFF
-  return (r,g,b)
+    v = int(hex_color[1:], 16)
+    b = v & 0xFF
+    g = (v >> 8) & 0xFF
+    r = (v >> 16) & 0xFF
+    return (r, g, b)
 
 
 def xml_escape(txt):
@@ -41,20 +43,21 @@ def xml_escape(txt):
 
 
 def visit_shapes(s, f):
-  f(s)
-  try:
-    for c in s.shapes:
-      visit_shapes(c, f)
-  except AttributeError:
-    pass
+    f(s)
+    try:
+        for c in s.shapes:
+            visit_shapes(c, f)
+    except AttributeError:
+        pass
+
 
 class SvgSurface(BaseSurface):
-  def __init__(self, fname, def_styles, padding=0, scale=1.0):
-    BaseSurface.__init__(self, fname, def_styles, padding, scale)
+    def __init__(self, fname, def_styles, padding=0, scale=1.0):
+        BaseSurface.__init__(self, fname, def_styles, padding, scale)
 
-    self.fh = None
+        self.fh = None
 
-  svg_header = '''
+    svg_header = '''
 
 ')
-
-
-  def text_bbox(self, text, font_params, spacing=0):
-    return CairoSurface.cairo_text_bbox(text, font_params, spacing, self.scale)
-
-  @staticmethod
-  def convert_pango_markup(text):
-    t = '{}'.format(text)
-    root = ET.fromstring(t)
-    # Convert  to 
-    for child in root:
-      if child.tag == 'span':
-        child.tag = 'tspan'
-        if 'foreground' in child.attrib:
-          child.attrib['fill'] = child.attrib['foreground']
-          del child.attrib['foreground']
-    return ET.tostring(root)[3:-4].decode('utf-8')
-
-  @staticmethod
-  def draw_text(x, y, text, css_class, text_color, baseline, anchor, anchor_off, spacing, fh):
-    ah, av = anchor
-
-    if ah == 'w':
-      text_anchor = 'normal'
-    elif ah == 'e':
-      text_anchor = 'end'
-    else:
-      text_anchor = 'middle'
-
-    attrs = {
-      'text-anchor': text_anchor,
-      'dy': baseline + anchor_off[1]
-    }
-
-
-    if text_color != (0,0,0):
-      attrs['style'] = 'fill:{}'.format(rgb_to_hex(text_color))
-
-    attributes = ' '.join(['{}="{}"'.format(k,v) for k,v in attrs.items()])
-
-    text = SvgSurface.convert_pango_markup(text)
-
-    fh.write('{}\n'.format(css_class, x, y, attributes, text))
-
-
-  def draw_shape(self, shape, fh=None):
-    if fh is None:
-      fh = self.fh
-    default_pen = rgb_to_hex(self.def_styles.line_color)
-
-    attrs = {
-      'stroke': 'none',
-      'fill': 'none'
-    }
-
-    weight = shape.param('weight', self.def_styles)
-    fill = shape.param('fill', self.def_styles)
-    line_color = shape.param('line_color', self.def_styles)
-    #line_cap = cairo_line_cap(shape.param('line_cap', self.def_styles))
-
-    stroke = True if weight > 0 else False
-
-    if weight > 0:
-      attrs['stroke-width'] = weight
-
-      if line_color is not None:
-        attrs['stroke'] = rgb_to_hex(line_color)
-        if len(line_color) == 4:
-          attrs['stroke-opacity'] = line_color[3] / 255.0
-      else:
-        attrs['stroke'] = default_pen
-
-
-    if fill is not None:
-      attrs['fill'] = rgb_to_hex(fill)
-      if len(fill) == 4:
-        attrs['fill-opacity'] = fill[3] / 255.0
-
-    #c.set_line_width(weight)
-    #c.set_line_cap(line_cap)
-
-    # Draw custom shapes
-    if shape.__class__ in self.shape_drawers:
-      self.shape_drawers[shape.__class__](shape, self)
-
-    # Draw standard shapes
-    elif isinstance(shape, GroupShape):
-      tform = ['translate({},{})'.format(*shape._pos)]
-
-      if 'scale' in shape.options:
-        tform.append('scale({})'.format(shape.options['scale']))
-      if 'angle' in shape.options:
-        tform.append('rotate({})'.format(shape.options['angle']))
-
-      fh.write('\n'.format(' '.join(tform)))
-
-      for s in shape.shapes:
-        self.draw_shape(s)
-
-      fh.write('\n')
-
-    elif isinstance(shape, TextShape):
-      x0, y0, x1, y1 = shape.points
-      baseline = shape._baseline
-
-      text = shape.param('text', self.def_styles)
-      font = shape.param('font', self.def_styles)
-      text_color = shape.param('text_color', self.def_styles)
-      #anchor = shape.param('anchor', self.def_styles).lower()
-      spacing = shape.param('spacing', self.def_styles)
-      css_class = shape.param('css_class')
-
-      anchor = shape.anchor_decode
-      anchor_off = shape._anchor_off
-      SvgSurface.draw_text(x0, y0, text, css_class, text_color, baseline, anchor, anchor_off, spacing, fh)
-
-
-    elif isinstance(shape, LineShape):
-      x0, y0, x1, y1 = shape.points
-
-      marker = shape.param('marker')
-      marker_start = shape.param('marker_start')
-      marker_seg = shape.param('marker_segment')
-      marker_end = shape.param('marker_end')
-      if marker is not None:
-        if marker_start is None:
-          marker_start = marker
-        if marker_end is None:
-          marker_end = marker
-        if marker_seg is None:
-          marker_seg = marker
-
-      adjust = shape.param('marker_adjust')
-      if adjust is None:
-        adjust = 0
-
-      if adjust > 0:
-        angle = math.atan2(y1-y0, x1-x0)
-        dx = math.cos(angle)
-        dy = math.sin(angle)
-
-        if marker_start in self.markers:
-          # Get bbox of marker
-          m_shape, ref, orient, units = self.markers[marker_start]
-          mx0, my0, mx1, my1 = m_shape.bbox
-          soff = (ref[0] - mx0) * adjust
-          if units == 'stroke' and weight > 0:
-            soff *= weight
-
-          # Move start point
-          x0 += soff * dx
-          y0 += soff * dy
-
-        if marker_end in self.markers:
-          # Get bbox of marker
-          m_shape, ref, orient, units = self.markers[marker_end]
-          mx0, my0, mx1, my1 = m_shape.bbox
-          eoff = (mx1 - ref[0]) * adjust
-          if units == 'stroke' and weight > 0:
-            eoff *= weight
-
-          # Move end point
-          x1 -= eoff * dx
-          y1 -= eoff * dy
-
-
-      # Add markers
-      if marker_start in self.markers:
-        attrs['marker-start'] = 'url(#{})'.format(marker_start)
-      if marker_end in self.markers:
-        attrs['marker-end'] = 'url(#{})'.format(marker_end)
-      # FIXME: marker_seg
-
-      attributes = ' '.join(['{}="{}"'.format(k,v) for k,v in attrs.items()])
-
-      fh.write('\n'.format(x0,y0, x1,y1, attributes))
-
-
-    elif isinstance(shape, RectShape):
-      x0, y0, x1, y1 = shape.points
-
-      attributes = ' '.join(['{}="{}"'.format(k,v) for k,v in attrs.items()])
-
-      fh.write('\n'.format(
-        x0,y0, x1-x0, y1-y0, attributes))
-
-    elif isinstance(shape, OvalShape):
-      x0, y0, x1, y1 = shape.points
-      xc = (x0 + x1) / 2.0
-      yc = (y0 + y1) / 2.0
-      w = abs(x1 - x0)
-      h = abs(y1 - y0)
-      rad = min(w,h)
-
-      attributes = ' '.join(['{}="{}"'.format(k,v) for k,v in attrs.items()])
-      fh.write('\n'.format(xc, yc,
-              w/2.0, h/2.0, attributes))
-
-
-    elif isinstance(shape, ArcShape):
-      x0, y0, x1, y1 = shape.points
-      xc = (x0 + x1) / 2.0
-      yc = (y0 + y1) / 2.0
-      #rad = abs(x1 - x0) / 2.0
-      w = abs(x1 - x0)
-      h = abs(y1 - y0)
-      xr = w / 2.0
-      yr = h / 2.0
-
-      closed = 'z' if shape.options['closed'] else ''
-      start = shape.options['start'] % 360
-      extent = shape.options['extent']
-      stop = (start + extent) % 360
-
-      #print('## ARC:', start, extent, stop)
-
-      # Start and end angles
-      sa = math.radians(start)
-      ea = math.radians(stop)
-
-      xs = xc + xr * math.cos(sa)
-      ys = yc - yr * math.sin(sa)
-      xe = xc + xr * math.cos(ea)
-      ye = yc - yr * math.sin(ea)
-
-      lflag = 0 if abs(extent) <= 180 else 1
-      sflag = 0 if extent >= 0 else 1
-
-      attributes = ' '.join(['{}="{}"'.format(k,v) for k,v in attrs.items()])
+                                                                               text_color, family, size, weight, style))
+
+        font_styles = '\n'.join(font_css)
+
+        # Determine which markers are in use
+        class MarkerVisitor(object):
+            def __init__(self):
+                self.markers = set()
+
+            def get_marker_info(self, s):
+                mark = s.param('marker')
+                if mark:
+                    self.markers.add(mark)
+                mark = s.param('marker_start')
+                if mark:
+                    self.markers.add(mark)
+                mark = s.param('marker_segment')
+                if mark:
+                    self.markers.add(mark)
+                mark = s.param('marker_end')
+                if mark:
+                    self.markers.add(mark)
+
+        mv = MarkerVisitor()
+        visit_shapes(canvas, mv.get_marker_info)
+        used_markers = mv.markers.intersection(set(self.markers.keys()))
+
+        # Generate markers
+        markers = []
+        for mname in used_markers:
+
+            m_shape, ref, orient, units = self.markers[mname]
+            mx0, my0, mx1, my1 = m_shape.bbox
+
+            mw = mx1 - mx0
+            mh = my1 - my0
+
+            # Unfortunately it looks like browser SVG rendering doesn't properly support
+            # marker viewBox that doesn't have an origin at 0,0 but Eye of Gnome does.
+
+            attrs = {
+                'id': mname,
+                'markerWidth': mw,
+                'markerHeight': mh,
+                'viewBox': ' '.join(str(p) for p in (0, 0, mw, mh)),
+                'refX': ref[0] - mx0,
+                'refY': ref[1] - my0,
+                'orient': orient,
+                'markerUnits': 'strokeWidth' if units == 'stroke' else 'userSpaceOnUse'
+            }
+
+            attributes = ' '.join(['{}="{}"'.format(k, v) for k, v in attrs.items()])
+
+            buf = io.StringIO()
+            self.draw_shape(m_shape, buf)
+            # Shift enerything inside a group so that the viewBox origin is 0,0
+            svg_shapes = '{}\n'.format(-mx0, -my0, buf.getvalue())
+            buf.close()
+
+            markers.append('\n{}'.format(attributes, svg_shapes))
+
+        markers = '\n'.join(markers)
+
+        if self.draw_bbox:
+            last = len(canvas.shapes)
+            for s in canvas.shapes[:last]:
+                bbox = s.bbox
+                r = canvas.create_rectangle(*bbox, line_color=(255, 0, 0, 127), fill=(0, 255, 0, 90))
+
+        with io.open(self.fname, 'w', encoding='utf-8') as fh:
+            self.fh = fh
+            fh.write(SvgSurface.svg_header.format(int(W * self.scale), int(H * self.scale),
+                                                  vbox, font_styles, markers))
+            if not transparent:
+                fh.write(''.format(x0 - self.padding, y0 - self.padding))
+            for s in canvas.shapes:
+                self.draw_shape(s)
+            fh.write('')
+
+    def text_bbox(self, text, font_params, spacing=0):
+        return CairoSurface.cairo_text_bbox(text, font_params, spacing, self.scale)
+
+    @staticmethod
+    def convert_pango_markup(text):
+        t = '{}'.format(text)
+        root = ET.fromstring(t)
+        # Convert  to 
+        for child in root:
+            if child.tag == 'span':
+                child.tag = 'tspan'
+                if 'foreground' in child.attrib:
+                    child.attrib['fill'] = child.attrib['foreground']
+                    del child.attrib['foreground']
+        return ET.tostring(root)[3:-4].decode('utf-8')
+
+    @staticmethod
+    def draw_text(x, y, text, css_class, text_color, baseline, anchor, anchor_off, spacing, fh):
+        ah, av = anchor
+
+        if ah == 'w':
+            text_anchor = 'normal'
+        elif ah == 'e':
+            text_anchor = 'end'
+        else:
+            text_anchor = 'middle'
+
+        attrs = {
+            'text-anchor': text_anchor,
+            'dy': baseline + anchor_off[1]
+        }
+
+        if text_color != (0, 0, 0):
+            attrs['style'] = 'fill:{}'.format(rgb_to_hex(text_color))
+
+        attributes = ' '.join(['{}="{}"'.format(k, v) for k, v in attrs.items()])
+
+        text = SvgSurface.convert_pango_markup(text)
+
+        fh.write('{}\n'.format(css_class, x, y, attributes, text))
+
+    def draw_shape(self, shape, fh=None):
+        if fh is None:
+            fh = self.fh
+        default_pen = rgb_to_hex(self.def_styles.line_color)
+
+        attrs = {
+            'stroke': 'none',
+            'fill': 'none'
+        }
+
+        weight = shape.param('weight', self.def_styles)
+        fill = shape.param('fill', self.def_styles)
+        line_color = shape.param('line_color', self.def_styles)
+        #line_cap = cairo_line_cap(shape.param('line_cap', self.def_styles))
+
+        stroke = True if weight > 0 else False
+
+        if weight > 0:
+            attrs['stroke-width'] = weight
+
+            if line_color is not None:
+                attrs['stroke'] = rgb_to_hex(line_color)
+                if len(line_color) == 4:
+                    attrs['stroke-opacity'] = line_color[3] / 255.0
+            else:
+                attrs['stroke'] = default_pen
+
+        if fill is not None:
+            attrs['fill'] = rgb_to_hex(fill)
+            if len(fill) == 4:
+                attrs['fill-opacity'] = fill[3] / 255.0
+
+        # c.set_line_width(weight)
+        # c.set_line_cap(line_cap)
+
+        # Draw custom shapes
+        if shape.__class__ in self.shape_drawers:
+            self.shape_drawers[shape.__class__](shape, self)
+
+        # Draw standard shapes
+        elif isinstance(shape, GroupShape):
+            tform = ['translate({},{})'.format(*shape._pos)]
+
+            if 'scale' in shape.options:
+                tform.append('scale({})'.format(shape.options['scale']))
+            if 'angle' in shape.options:
+                tform.append('rotate({})'.format(shape.options['angle']))
+
+            fh.write('\n'.format(' '.join(tform)))
+
+            for s in shape.shapes:
+                self.draw_shape(s)
+
+            fh.write('\n')
+
+        elif isinstance(shape, TextShape):
+            x0, y0, x1, y1 = shape.points
+            baseline = shape._baseline
+
+            text = shape.param('text', self.def_styles)
+            font = shape.param('font', self.def_styles)
+            text_color = shape.param('text_color', self.def_styles)
+            #anchor = shape.param('anchor', self.def_styles).lower()
+            spacing = shape.param('spacing', self.def_styles)
+            css_class = shape.param('css_class')
+
+            anchor = shape.anchor_decode
+            anchor_off = shape._anchor_off
+            SvgSurface.draw_text(x0, y0, text, css_class, text_color, baseline, anchor, anchor_off, spacing, fh)
+
+        elif isinstance(shape, LineShape):
+            x0, y0, x1, y1 = shape.points
+
+            marker = shape.param('marker')
+            marker_start = shape.param('marker_start')
+            marker_seg = shape.param('marker_segment')
+            marker_end = shape.param('marker_end')
+            if marker is not None:
+                if marker_start is None:
+                    marker_start = marker
+                if marker_end is None:
+                    marker_end = marker
+                if marker_seg is None:
+                    marker_seg = marker
+
+            adjust = shape.param('marker_adjust')
+            if adjust is None:
+                adjust = 0
+
+            if adjust > 0:
+                angle = math.atan2(y1 - y0, x1 - x0)
+                dx = math.cos(angle)
+                dy = math.sin(angle)
+
+                if marker_start in self.markers:
+                    # Get bbox of marker
+                    m_shape, ref, orient, units = self.markers[marker_start]
+                    mx0, my0, mx1, my1 = m_shape.bbox
+                    soff = (ref[0] - mx0) * adjust
+                    if units == 'stroke' and weight > 0:
+                        soff *= weight
+
+                    # Move start point
+                    x0 += soff * dx
+                    y0 += soff * dy
+
+                if marker_end in self.markers:
+                    # Get bbox of marker
+                    m_shape, ref, orient, units = self.markers[marker_end]
+                    mx0, my0, mx1, my1 = m_shape.bbox
+                    eoff = (mx1 - ref[0]) * adjust
+                    if units == 'stroke' and weight > 0:
+                        eoff *= weight
+
+                    # Move end point
+                    x1 -= eoff * dx
+                    y1 -= eoff * dy
+
+            # Add markers
+            if marker_start in self.markers:
+                attrs['marker-start'] = 'url(#{})'.format(marker_start)
+            if marker_end in self.markers:
+                attrs['marker-end'] = 'url(#{})'.format(marker_end)
+            # FIXME: marker_seg
+
+            attributes = ' '.join(['{}="{}"'.format(k, v) for k, v in attrs.items()])
+
+            fh.write('\n'.format(x0, y0, x1, y1, attributes))
+
+        elif isinstance(shape, RectShape):
+            x0, y0, x1, y1 = shape.points
+
+            attributes = ' '.join(['{}="{}"'.format(k, v) for k, v in attrs.items()])
+
+            fh.write('\n'.format(
+                x0, y0, x1 - x0, y1 - y0, attributes))
+
+        elif isinstance(shape, OvalShape):
+            x0, y0, x1, y1 = shape.points
+            xc = (x0 + x1) / 2.0
+            yc = (y0 + y1) / 2.0
+            w = abs(x1 - x0)
+            h = abs(y1 - y0)
+            rad = min(w, h)
+
+            attributes = ' '.join(['{}="{}"'.format(k, v) for k, v in attrs.items()])
+            fh.write('\n'.format(xc, yc,
+                                                                              w / 2.0, h / 2.0, attributes))
+
+        elif isinstance(shape, ArcShape):
+            x0, y0, x1, y1 = shape.points
+            xc = (x0 + x1) / 2.0
+            yc = (y0 + y1) / 2.0
+            #rad = abs(x1 - x0) / 2.0
+            w = abs(x1 - x0)
+            h = abs(y1 - y0)
+            xr = w / 2.0
+            yr = h / 2.0
+
+            closed = 'z' if shape.options['closed'] else ''
+            start = shape.options['start'] % 360
+            extent = shape.options['extent']
+            stop = (start + extent) % 360
+
+            # print('## ARC:', start, extent, stop)
+
+            # Start and end angles
+            sa = math.radians(start)
+            ea = math.radians(stop)
+
+            xs = xc + xr * math.cos(sa)
+            ys = yc - yr * math.sin(sa)
+            xe = xc + xr * math.cos(ea)
+            ye = yc - yr * math.sin(ea)
+
+            lflag = 0 if abs(extent) <= 180 else 1
+            sflag = 0 if extent >= 0 else 1
+
+            attributes = ' '.join(['{}="{}"'.format(k, v) for k, v in attrs.items()])
 
 #      fh.write(u'\n'.format(xc, yc, rgb_to_hex((255,0,255))))
 #      fh.write(u'\n'.format(xs, ys, rgb_to_hex((0,0,255))))
 #      fh.write(u'\n'.format(xe, ye, rgb_to_hex((0,255,255))))
 
-      fh.write('\n'.format(xs,ys, xr,yr, lflag, sflag, xe,ye, closed, attributes))
-
-    elif isinstance(shape, PathShape):
-      pp = shape.nodes[0]
-      nl = []
-
-      for i, n in enumerate(shape.nodes):
-        if n == 'z':
-          nl.append('z')
-          break
-        elif len(n) == 2:
-          cmd = 'L' if i > 0 else 'M'
-          nl.append('{} {} {}'.format(cmd, *n))
-          pp = n
-        elif len(n) == 6:
-          nl.append('C {} {}, {} {}, {} {}'.format(*n))
-          pp = n[4:6]
-        elif len(n) == 5: # Arc (javascript arcto() args)
-          #print('# arc:', pp)
-          #pp = self.draw_rounded_corner(pp, n[0:2], n[2:4], n[4], c)
-
-          center, start_p, end_p, rad = rounded_corner(pp, n[0:2], n[2:4], n[4])
-          if rad < 0: # No arc
-            print('## Rad < 0')
-            #c.line_to(*end_p)
-            nl.append('L {} {}'.format(*end_p))
-          else:
-            # Determine angles to arc end points
-            ostart_p = (start_p[0] - center[0], start_p[1] - center[1])
-            oend_p = (end_p[0] - center[0], end_p[1] - center[1])
-            start_a = math.atan2(ostart_p[1], ostart_p[0]) % math.radians(360)
-            end_a = math.atan2(oend_p[1], oend_p[0]) % math.radians(360)
-
-            # Determine direction of arc
-            # Rotate whole system so that start_a is on x-axis
-            # Then if delta < 180 cw  if delta > 180 ccw
-            delta = (end_a - start_a) % math.radians(360)
-
-            if delta < math.radians(180): # CW
-              sflag = 1
-            else: # CCW
-              sflag = 0
-
-            nl.append('L {} {}'.format(*start_p))
-            #nl.append('L {} {}'.format(*end_p))
-            nl.append('A {} {} 0 0 {} {} {}'.format(rad, rad, sflag, *end_p))
-
-
-            #print('# start_a, end_a', math.degrees(start_a), math.degrees(end_a),
-            #            math.degrees(delta))
-            #fh.write(u'\n'.format(center[0], center[1], rad))
-          pp = end_p
-
-          #print('# pp:', pp)
-
-      attributes = ' '.join(['{}="{}"'.format(k,v) for k,v in attrs.items()])
-      fh.write('\n'.format(' '.join(nl), attributes))
+            fh.write('\n'.format(xs, ys, xr, yr, lflag, sflag, xe, ye, closed, attributes))
+
+        elif isinstance(shape, PathShape):
+            pp = shape.nodes[0]
+            nl = []
+
+            for i, n in enumerate(shape.nodes):
+                if n == 'z':
+                    nl.append('z')
+                    break
+                elif len(n) == 2:
+                    cmd = 'L' if i > 0 else 'M'
+                    nl.append('{} {} {}'.format(cmd, *n))
+                    pp = n
+                elif len(n) == 6:
+                    nl.append('C {} {}, {} {}, {} {}'.format(*n))
+                    pp = n[4:6]
+                elif len(n) == 5:  # Arc (javascript arcto() args)
+                    # print('# arc:', pp)
+                    #pp = self.draw_rounded_corner(pp, n[0:2], n[2:4], n[4], c)
+
+                    center, start_p, end_p, rad = rounded_corner(pp, n[0:2], n[2:4], n[4])
+                    if rad < 0:  # No arc
+                        print('## Rad < 0')
+                        # c.line_to(*end_p)
+                        nl.append('L {} {}'.format(*end_p))
+                    else:
+                        # Determine angles to arc end points
+                        ostart_p = (start_p[0] - center[0], start_p[1] - center[1])
+                        oend_p = (end_p[0] - center[0], end_p[1] - center[1])
+                        start_a = math.atan2(ostart_p[1], ostart_p[0]) % math.radians(360)
+                        end_a = math.atan2(oend_p[1], oend_p[0]) % math.radians(360)
+
+                        # Determine direction of arc
+                        # Rotate whole system so that start_a is on x-axis
+                        # Then if delta < 180 cw  if delta > 180 ccw
+                        delta = (end_a - start_a) % math.radians(360)
+
+                        if delta < math.radians(180):  # CW
+                            sflag = 1
+                        else:  # CCW
+                            sflag = 0
+
+                        nl.append('L {} {}'.format(*start_p))
+                        #nl.append('L {} {}'.format(*end_p))
+                        nl.append('A {} {} 0 0 {} {} {}'.format(rad, rad, sflag, *end_p))
+
+                        # print('# start_a, end_a', math.degrees(start_a), math.degrees(end_a),
+                        #            math.degrees(delta))
+                        # fh.write(u'\n'.format(center[0], center[1], rad))
+                    pp = end_p
+
+                    # print('# pp:', pp)
+
+            attributes = ' '.join(['{}="{}"'.format(k, v) for k, v in attrs.items()])
+            fh.write('\n'.format(' '.join(nl), attributes))
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..6e650e2
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,40 @@
+[metadata]
+name = symbolator
+author = Kevin Thibedeau
+author_email = kevin.thibedeau@gmail.com
+url = http://kevinpt.github.io/symbolator
+download_url = http://kevinpt.github.io/symbolator
+description = HDL symbol generator
+long_description = file: README.rst
+description_file = README.rst
+version = attr: symbolator.__version__
+license = MIT
+keywords = HDL symbol
+classifiers =
+    Development Status :: 5 - Production/Stable
+    Operating System :: OS Independent
+    Intended Audience :: Developers
+    Topic :: Multimedia :: Graphics
+    Topic :: Software Development :: Documentation
+    Natural Language :: English
+    Programming Language :: Python :: 3
+    License :: OSI Approved :: MIT License
+
+[options]
+packages =
+    nucanvas
+    nucanvas/color
+    symbolator_sphinx
+py_modules = symbolator
+install_requires =
+    sphinx>=4.3,<5
+    hdlparse @ git+https://github.com/kammoh/pyHDLParser.git
+include_package_data = True
+
+[options.entry_points]
+console_scripts =
+    symbolator = symbolator:main
+
+[pycodestyle]
+max_line_length = 120
+ignore = E501
diff --git a/setup.py b/setup.py
old mode 100755
new mode 100644
index 4db9fd8..6068493
--- a/setup.py
+++ b/setup.py
@@ -1,60 +1,3 @@
+from setuptools import setup
 
-import sys
-
-try:
-  from setuptools import setup
-except ImportError:
-	sys.exit('ERROR: setuptools is required.\nTry using "pip install setuptools".')
-
-# Use README.rst for the long description
-with open('README.rst') as fh:
-    long_description = fh.read()
-
-def get_package_version(verfile):
-  '''Scan the script for the version string'''
-  version = None
-  with open(verfile) as fh:
-      try:
-          version = [line.split('=')[1].strip().strip("'") for line in fh if \
-              line.startswith('__version__')][0]
-      except IndexError:
-          pass
-  return version
-
-version = get_package_version('symbolator.py')
-
-if version is None:
-    raise RuntimeError('Unable to find version string in file: {0}'.format(version_file))
-
-
-setup(name='symbolator',
-    version=version,
-    author='Kevin Thibedeau',
-    author_email='kevin.thibedeau@gmail.com',
-    url='http://kevinpt.github.io/symbolator',
-    download_url='http://kevinpt.github.io/symbolator',
-    description='HDL symbol generator',
-    long_description=long_description,
-    platforms = ['Any'],
-    install_requires = ['hdlparse>=1.0.4'],
-    packages = ['nucanvas', 'nucanvas/color', 'symbolator_sphinx'],
-    py_modules = ['symbolator'],
-    entry_points = {
-        'console_scripts': ['symbolator = symbolator:main']
-    },
-    include_package_data = True,
-
-    use_2to3 = False,
-
-    keywords='HDL symbol',
-    license='MIT',
-    classifiers=['Development Status :: 5 - Production/Stable',
-        'Operating System :: OS Independent',
-        'Intended Audience :: Developers',
-        'Topic :: Multimedia :: Graphics',
-        'Topic :: Software Development :: Documentation',
-        'Natural Language :: English',
-        'Programming Language :: Python :: 3',
-        'License :: OSI Approved :: MIT License'
-        ]
-    )
+setup()
diff --git a/symbolator.py b/symbolator.py
index 36470d8..bc2fd18 100755
--- a/symbolator.py
+++ b/symbolator.py
@@ -1,10 +1,15 @@
-#!/usr/bin/python
+#!/usr/bin/env python3
 # -*- coding: utf-8 -*-
 # Copyright © 2017 Kevin Thibedeau
 # Distributed under the terms of the MIT license
 
-
-import sys, copy, re, argparse, os, errno
+import sys
+import re
+import argparse
+import os
+import logging
+import textwrap
+from typing import Any, Iterator, List, Type
 
 from nucanvas import DrawStyle, NuCanvas
 from nucanvas.cairo_backend import CairoSurface
@@ -15,580 +20,774 @@
 import hdlparse.vhdl_parser as vhdl
 import hdlparse.verilog_parser as vlog
 
-from hdlparse.vhdl_parser import VhdlComponent
+from hdlparse.vhdl_parser import VhdlComponent, VhdlEntity, VhdlParameterType
+
+__version__ = "1.1.0"
 
-__version__ = '1.0.2'
+log = logging.getLogger(__name__)
 
 
 def xml_escape(txt):
-  '''Replace special characters for XML strings'''
-  txt = txt.replace('&', '&')
-  txt = txt.replace('<', '<')
-  txt = txt.replace('>', '>')
-  txt = txt.replace('"', '"')
-  return txt
+    """Replace special characters for XML strings"""
+    txt = txt.replace("&", "&")
+    txt = txt.replace("<", "<")
+    txt = txt.replace(">", ">")
+    txt = txt.replace('"', """)
+    return txt
 
 
 class Pin(object):
-  '''Symbol pin'''
-  def __init__(self, text, side='l', bubble=False, clocked=False, bus=False, bidir=False, data_type=None):
-    self.text = text
-    self.bubble = bubble
-    self.side = side
-    self.clocked = clocked
-    self.bus = bus
-    self.bidir = bidir
-    self.data_type = data_type
-
-    self.pin_length = 20
-    self.bubble_rad = 3
-    self.padding = 10
-
-  @property
-  def styled_text(self):
-    return re.sub(r'(\[.*\])', r'\1', xml_escape(self.text))
-
-  @property
-  def styled_type(self):
-    if self.data_type:
-      return re.sub(r'(\[.*\])', r'\1', xml_escape(self.data_type))
-    else:
-      return None
-
-
-  def draw(self, x, y, c):
-    g = c.create_group(x,y)
-    #r = self.bubble_rad
-
-    if self.side == 'l':
-      xs = -self.pin_length
-      #bx = -r
-      #xe = 2*bx if self.bubble else 0
-      xe = 0
-    else:
-      xs = self.pin_length
-      #bx = r
-      #xe = 2*bx if self.bubble else 0
-      xe = 0
-
-    # Whisker for pin
-    pin_weight = 3 if self.bus else 1
-    ls = g.create_line(xs,0, xe,0, weight=pin_weight)
-
-    if self.bidir:
-      ls.options['marker_start'] = 'arrow_back'
-      ls.options['marker_end'] = 'arrow_fwd'
-      ls.options['marker_adjust'] = 0.8
-
-    if self.bubble:
-      #g.create_oval(bx-r,-r, bx+r, r, fill=(255,255,255))
-      ls.options['marker_end'] = 'bubble'
-      ls.options['marker_adjust'] = 1.0
-
-    if self.clocked: # Draw triangle for clock
-      ls.options['marker_end'] = 'clock'
-      #ls.options['marker_adjust'] = 1.0
-
-    if self.side == 'l':
-      g.create_text(self.padding,0, anchor='w', text=self.styled_text)
-
-      if self.data_type:
-        g.create_text(xs-self.padding, 0, anchor='e', text=self.styled_type, text_color=(150,150,150))
-
-    else: # Right side pin
-      g.create_text(-self.padding,0, anchor='e', text=self.styled_text)
-
-      if self.data_type:
-        g.create_text(xs+self.padding, 0, anchor='w', text=self.styled_type, text_color=(150,150,150))
-
-    return g
-
-  def text_width(self, c, font_params):
-    x0, y0, x1, y1, baseline = c.surf.text_bbox(self.text, font_params)
-    w = abs(x1 - x0)
-    return self.padding + w
-
-
-class PinSection(object):
-  '''Symbol section'''
-  def __init__(self, name, fill=None, line_color=(0,0,0)):
-    self.fill = fill
-    self.line_color = line_color
-    self.pins = []
-    self.spacing = 20
-    self.padding = 5
-    self.show_name = True
-
-    self.name = name
-    self.sect_class = None
-
-    if name is not None:
-      m = re.match(r'^(\w+)\s*\|(.*)$', name)
-      if m:
-        self.name = m.group(2).strip()
-        self.sect_class = m.group(1).strip().lower()
-        if len(self.name) == 0:
-          self.name = None
-
-    class_colors = {
-      'clocks': sinebow.lighten(sinebow.sinebow(0), 0.75),     # Red
-      'data': sinebow.lighten(sinebow.sinebow(0.35), 0.75),    # Green
-      'control': sinebow.lighten(sinebow.sinebow(0.15), 0.75), # Yellow
-      'power': sinebow.lighten(sinebow.sinebow(0.07), 0.75)    # Orange
-    }
-
-    if self.sect_class in class_colors:
-      self.fill = class_colors[self.sect_class]
-
-  def add_pin(self, p):
-    self.pins.append(p)
-
-  @property
-  def left_pins(self):
-    return [p for p in self.pins if p.side == 'l']
+    """Symbol pin"""
+
+    def __init__(
+        self,
+        text,
+        side="l",
+        bubble=False,
+        clocked=False,
+        bus=False,
+        bidir=False,
+        data_type=None,
+    ):
+        self.text = text
+        self.bubble = bubble
+        self.side = side
+        self.clocked = clocked
+        self.bus = bus
+        self.bidir = bidir
+        self.data_type = data_type
+
+        self.pin_length = 20
+        self.bubble_rad = 3
+        self.padding = 10
+
+    @property
+    def styled_text(self):
+        return re.sub(
+            r"(\[.*\])", r'\1', xml_escape(self.text)
+        )
+
+    @property
+    def styled_type(self):
+        if self.data_type:
+            return re.sub(
+                r"(\[.*\])",
+                r'\1',
+                xml_escape(self.data_type),
+            )
+        else:
+            return None
+
+    def draw(self, x, y, c):
+        g = c.create_group(x, y)
+        # r = self.bubble_rad
+
+        if self.side == "l":
+            xs = -self.pin_length
+            # bx = -r
+            # xe = 2*bx if self.bubble else 0
+            xe = 0
+        else:
+            xs = self.pin_length
+            # bx = r
+            # xe = 2*bx if self.bubble else 0
+            xe = 0
+
+        # Whisker for pin
+        pin_weight = 3 if self.bus else 1
+        ls = g.create_line(xs, 0, xe, 0, weight=pin_weight)
+
+        if self.bidir:
+            ls.options["marker_start"] = "arrow_back"
+            ls.options["marker_end"] = "arrow_fwd"
+            ls.options["marker_adjust"] = 0.8
+
+        if self.bubble:
+            # g.create_oval(bx-r,-r, bx+r, r, fill=(255,255,255))
+            ls.options["marker_end"] = "bubble"
+            ls.options["marker_adjust"] = 1.0
+
+        if self.clocked:  # Draw triangle for clock
+            ls.options["marker_end"] = "clock"
+            # ls.options['marker_adjust'] = 1.0
+
+        if self.side == "l":
+            g.create_text(self.padding, 0, anchor="w", text=self.styled_text)
+
+            if self.data_type:
+                g.create_text(
+                    xs - self.padding,
+                    0,
+                    anchor="e",
+                    text=self.styled_type,
+                    text_color=(150, 150, 150),
+                )
+
+        else:  # Right side pin
+            g.create_text(-self.padding, 0, anchor="e", text=self.styled_text)
+
+            if self.data_type:
+                g.create_text(
+                    xs + self.padding,
+                    0,
+                    anchor="w",
+                    text=self.styled_type,
+                    text_color=(150, 150, 150),
+                )
+
+        return g
+
+    def text_width(self, c, font_params):
+        x0, y0, x1, y1, baseline = c.surf.text_bbox(self.text, font_params)
+        w = abs(x1 - x0)
+        return self.padding + w
+
+
+class PinSection:
+    """Symbol section"""
+
+    def __init__(
+        self,
+        name,
+        fill=None,
+        line_color=(0, 0, 0),
+        title_font=("Verdana", 9, "bold"),
+        class_colors={},
+    ):
+        self.fill = fill
+        self.line_color = line_color
+        self.title_font = title_font
+        self.pins = []
+        self.spacing = 20
+        self.padding = 5
+        self.show_name = True
+        self.name = name
+        self.sect_class = None
+
+        if class_colors is None:
+            class_colors = {
+                "clocks": sinebow.lighten(sinebow.sinebow(0), 0.75),  # Red
+                "data": sinebow.lighten(sinebow.sinebow(0.35), 0.75),  # Green
+                "control": sinebow.lighten(sinebow.sinebow(0.15), 0.75),  # Yellow
+                "power": sinebow.lighten(sinebow.sinebow(0.07), 0.75),  # Orange
+            }
+
+        if name is not None:
+            m = re.match(r"^([^\|]+)\s*(\|(\w*))?$", name)
+            if m:
+                self.name = m.group(3)
+                if self.name is not None:
+                    self.name = self.name.strip()
+                    if len(self.name) == 0:
+                        self.name = None
+                self.sect_class = m.group(1).strip().lower() if m.group(1) else None
+
+        # if self.sect_class in class_colors:
+        #     self.fill = class_colors[self.sect_class]
+        if self.sect_class:
+            m = re.match(
+                r"#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$",
+                self.sect_class,
+                re.IGNORECASE,
+            )
+            if m:
+                self.fill = [int(m.group(i), 16) for i in range(1, 4)]
+            elif self.sect_class in class_colors:
+                self.fill = class_colors[self.sect_class]
+
+    def add_pin(self, p):
+        self.pins.append(p)
+
+    @property
+    def left_pins(self):
+        return [p for p in self.pins if p.side == "l"]
+
+    @property
+    def right_pins(self):
+        return [p for p in self.pins if p.side == "r"]
+
+    @property
+    def rows(self):
+        return max(len(self.left_pins), len(self.right_pins))
+
+    def min_width(self, c, font_params):
+        try:
+            lmax = max(tw.text_width(c, font_params) for tw in self.left_pins)
+        except ValueError:
+            lmax = 0
+
+        try:
+            rmax = max(tw.text_width(c, font_params) for tw in self.right_pins)
+        except ValueError:
+            rmax = 0
+
+        if self.name is not None:
+            x0, y0, x1, y1, baseline = c.surf.text_bbox(self.name, font_params)
+            w = abs(x1 - x0)
+            name_width = self.padding + w
+
+            if lmax > 0:
+                lmax = max(lmax, name_width)
+            else:
+                rmax = max(rmax, name_width)
+
+        return lmax + rmax + self.padding
+
+    def draw(self, x, y, width, c):
+        dy = self.spacing
+
+        g = c.create_group(x, y)
+
+        toff = 0
+
+        # Compute title offset
+        if self.show_name and self.name:
+            x0, y0, x1, y1, baseline = c.surf.text_bbox(self.name, self.title_font)
+            toff = y1 - y0
+
+        top = -dy / 2 - self.padding
+        bot = toff - dy / 2 + self.rows * dy + self.padding
+        g.create_rectangle(
+            0, top, width, bot, fill=self.fill, line_color=self.line_color
+        )
+
+        if self.show_name and self.name:
+            g.create_text(width / 2.0, 0, text=self.name, font=self.title_font)
+
+        lp = self.left_pins
+        py = 0
+        for p in lp:
+            p.draw(0, toff + py, g)
+            py += dy
+
+        rp = self.right_pins
+        py = 0
+        for p in rp:
+            p.draw(0 + width, toff + py, g)
+            py += dy
+
+        return (g, (x, y + top, x + width, y + bot))
 
-  @property
-  def right_pins(self):
-    return [p for p in self.pins if p.side == 'r']
-
-  @property
-  def rows(self):
-    return max(len(self.left_pins), len(self.right_pins))
-
-  def min_width(self, c, font_params):
-    try:
-      lmax = max(tw.text_width(c, font_params) for tw in self.left_pins)
-    except ValueError:
-      lmax = 0
-
-    try:
-      rmax = max(tw.text_width(c, font_params) for tw in self.right_pins)
-    except ValueError:
-      rmax = 0
-
-    if self.name is not None:
-      x0, y0, x1, y1, baseline = c.surf.text_bbox(self.name, font_params)
-      w = abs(x1 - x0)
-      name_width = self.padding + w
-
-      if lmax > 0:
-        lmax = max(lmax, name_width)
-      else:
-        rmax = max(rmax, name_width)
-
-    return lmax + rmax + self.padding
-
-  def draw(self, x, y, width, c):
-    dy = self.spacing
-
-    g = c.create_group(x,y)
-
-    toff = 0
-
-    title_font = ('Times', 12, 'italic')
-    if self.show_name and self.name is not None and len(self.name) > 0: # Compute title offset
-      x0,y0, x1,y1, baseline = c.surf.text_bbox(self.name, title_font)
-      toff = y1 - y0
-
-    top = -dy/2 - self.padding
-    bot = toff - dy/2 + self.rows*dy + self.padding
-    g.create_rectangle(0,top, width,bot, fill=self.fill, line_color=self.line_color)
-
-    if self.show_name and self.name is not None:
-      g.create_text(width / 2.0,0, text=self.name, font=title_font)
-
-
-    lp = self.left_pins
-    py = 0
-    for p in lp:
-      p.draw(0, toff + py, g)
-      py += dy
-
-    rp = self.right_pins
-    py = 0
-    for p in rp:
-      p.draw(0 + width, toff + py, g)
-      py += dy
-
-    return (g, (x, y+top, x+width, y+bot))
 
 class Symbol(object):
-  '''Symbol composed of sections'''
-  def __init__(self, sections=None, line_color=(0,0,0)):
-    if sections is not None:
-      self.sections = sections
-    else:
-      self.sections = []
+    """Symbol composed of sections"""
+
+    def __init__(self, sections=None, line_color=(0, 0, 0)):
+        if sections is not None:
+            self.sections = sections
+        else:
+            self.sections = []
+
+        self.line_weight = 3
+        self.line_color = line_color
+
+    def add_section(self, section):
+        self.sections.append(section)
+
+    def draw(self, x, y, c, sym_width=None):
+        if sym_width is None:
+            style = c.surf.def_styles
+            sym_width = max(s.min_width(c, style.font) for s in self.sections)
+
+        # Draw each section
+        yoff = y
+        sect_boxes = []
+        for s in self.sections:
+            sg, sb = s.draw(x, yoff, sym_width, c)
+            bb = sg.bbox
+            yoff += bb[3] - bb[1]
+            sect_boxes.append(sb)
+            # section.draw(50, 100 + h, sym_width, nc)
+
+        # Find outline of all sections
+        hw = self.line_weight / 2.0 - 0.5
+        sect_boxes = list(zip(*sect_boxes))
+        x0 = min(sect_boxes[0]) + hw
+        y0 = min(sect_boxes[1]) + hw
+        x1 = max(sect_boxes[2]) - hw
+        y1 = max(sect_boxes[3]) - hw
+
+        # Add symbol outline
+        c.create_rectangle(
+            x0, y0, x1, y1, weight=self.line_weight, line_color=self.line_color
+        )
+
+        return (x0, y0, x1, y1)
 
-    self.line_weight = 3
-    self.line_color = line_color
-
-  def add_section(self, section):
-    self.sections.append(section)
-
-  def draw(self, x, y, c, sym_width=None):
-    if sym_width is None:
-      style = c.surf.def_styles
-      sym_width = max(s.min_width(c, style.font) for s in self.sections)
-
-    # Draw each section
-    yoff = y
-    sect_boxes = []
-    for s in self.sections:
-      sg, sb = s.draw(x, yoff, sym_width, c)
-      bb = sg.bbox
-      yoff += bb[3] - bb[1]
-      sect_boxes.append(sb)
-      #section.draw(50, 100 + h, sym_width, nc)
-
-    # Find outline of all sections
-    hw = self.line_weight / 2.0 - 0.5
-    sect_boxes = list(zip(*sect_boxes))
-    x0 = min(sect_boxes[0]) + hw
-    y0 = min(sect_boxes[1]) + hw
-    x1 = max(sect_boxes[2]) - hw
-    y1 = max(sect_boxes[3]) - hw
-
-    # Add symbol outline
-    c.create_rectangle(x0,y0,x1,y1, weight=self.line_weight, line_color=self.line_color)
-
-
-    return (x0,y0, x1,y1)
 
 class HdlSymbol(object):
-  '''Top level symbol object'''
-  def __init__(self, component=None, libname=None, symbols=None, symbol_spacing=10, width_steps=20):
-    self.symbols = symbols if symbols is not None else []
-    self.symbol_spacing = symbol_spacing
-    self.width_steps = width_steps
-    self.component = component
-    self.libname = libname
-
-
-
-  def add_symbol(self, symbol):
-    self.symbols.append(symbol)
-
-  def draw(self, x, y, c):
-    style = c.surf.def_styles
-    sym_width = max(s.min_width(c, style.font) for sym in self.symbols for s in sym.sections)
-
-    sym_width = (sym_width // self.width_steps + 1) * self.width_steps
-
-    yoff = y
-    for i, s in enumerate(self.symbols):
-      bb = s.draw(x, y + yoff, c, sym_width)
-      if i==0 and self.libname:
-        # Add libname
-        c.create_text((bb[0]+bb[2])/2.0,bb[1] - self.symbol_spacing, anchor='cs',
-          text=self.libname, font=('Helvetica', 12, 'bold'))
-      elif i == 0 and self.component:
-        # Add component name
-        c.create_text((bb[0]+bb[2])/2.0,bb[1] - self.symbol_spacing, anchor='cs',
-          text=self.component, font=('Helvetica', 12, 'bold'))
-
-      yoff += bb[3] - bb[1] + self.symbol_spacing
-    if self.libname is not None:
-        c.create_text((bb[0]+bb[2])/2.0,bb[3] + 2 * self.symbol_spacing, anchor='cs',
-          text=self.component, font=('Helvetica', 12, 'bold'))
-
+    """Top level symbol object"""
+
+    def __init__(
+        self,
+        component=None,
+        libname=None,
+        symbols=None,
+        symbol_spacing=10,
+        width_steps=20,
+    ):
+        self.symbols = symbols if symbols is not None else []
+        self.symbol_spacing = symbol_spacing
+        self.width_steps = width_steps
+        self.component = component
+        self.libname = libname
+
+    def add_symbol(self, symbol):
+        self.symbols.append(symbol)
+
+    def draw(self, x, y, c):
+        style = c.surf.def_styles
+        sym_width = max(
+            s.min_width(c, style.font) for sym in self.symbols for s in sym.sections
+        )
+
+        sym_width = (sym_width // self.width_steps + 1) * self.width_steps
+
+        yoff = y
+        for i, s in enumerate(self.symbols):
+            bb = s.draw(x, y + yoff, c, sym_width)
+            if i == 0 and self.libname:
+                # Add libname
+                c.create_text(
+                    (bb[0] + bb[2]) / 2.0,
+                    bb[1] - self.symbol_spacing,
+                    anchor="cs",
+                    text=self.libname,
+                    font=("Helvetica", 12, "bold"),
+                )
+            elif i == 0 and self.component:
+                # Add component name
+                c.create_text(
+                    (bb[0] + bb[2]) / 2.0,
+                    bb[1] - self.symbol_spacing,
+                    anchor="cs",
+                    text=self.component,
+                    font=("Helvetica", 12, "bold"),
+                )
+
+            yoff += bb[3] - bb[1] + self.symbol_spacing
+        if self.libname:
+            c.create_text(
+                (bb[0] + bb[2]) / 2.0,
+                bb[3] + 2 * self.symbol_spacing,
+                anchor="cs",
+                text=self.component,
+                font=("Helvetica", 12, "bold"),
+            )
 
 
 def make_section(sname, sect_pins, fill, extractor, no_type=False):
-  '''Create a section from a pin list'''
-  sect = PinSection(sname, fill=fill)
-  side = 'l'
-
-  for p in sect_pins:
-    pname = p.name
-    pdir = p.mode
-    data_type = p.data_type if no_type == False else None
-    bus = extractor.is_array(p.data_type)
-
-    pdir = pdir.lower()
-
-    # Convert Verilog modes
-    if pdir == 'input':
-      pdir = 'in'
-    if pdir == 'output':
-      pdir = 'out'
-
-    # Determine which side the pin is on
-    if pdir in ('in'):
-      side = 'l'
-    elif pdir in ('out', 'inout'):
-      side = 'r'
-
-    pin = Pin(pname, side=side, data_type=data_type)
-    if pdir == 'inout':
-      pin.bidir = True
-
-    # Check for pin name patterns
-    pin_patterns = {
-      'clock': re.compile(r'(^cl(oc)?k)|(cl(oc)?k$)', re.IGNORECASE),
-      'bubble': re.compile(r'_[nb]$', re.IGNORECASE),
-      'bus': re.compile(r'(\[.*\]$)', re.IGNORECASE)
-    }
-
-    if pdir == 'in' and pin_patterns['clock'].search(pname):
-      pin.clocked = True
-
-    if pin_patterns['bubble'].search(pname):
-      pin.bubble = True
-
-    if bus or pin_patterns['bus'].search(pname):
-      pin.bus = True
-
-    sect.add_pin(pin)
-
-  return sect
-
-def make_symbol(comp, extractor, title=False, libname="", no_type=False):
-  '''Create a symbol from a parsed component/module'''
-  if libname != "":
-      vsym = HdlSymbol(comp.name, libname)
-  elif title != False:
-      vsym = HdlSymbol(comp.name)
-  else:
-      vsym = HdlSymbol()
-
-  color_seq = sinebow.distinct_color_sequence(0.6)
-
-  if len(comp.generics) > 0: #'generic' in entity_data:
-    s = make_section(None, comp.generics, (200,200,200), extractor, no_type)
-    s.line_color = (100,100,100)
-    gsym = Symbol([s], line_color=(100,100,100))
-    vsym.add_symbol(gsym)
-  if len(comp.ports) > 0: #'port' in entity_data:
-    psym = Symbol()
-
-    # Break ports into sections
-    cur_sect = []
-    sections = []
-    sect_name = comp.sections[0] if 0 in comp.sections else None
-    for i,p in enumerate(comp.ports):
-      if i in comp.sections and len(cur_sect) > 0: # Finish previous section
-        sections.append((sect_name, cur_sect))
+    """Create a section from a pin list"""
+    sect = PinSection(sname, fill=fill)
+
+    for p in sect_pins:
+        pname = p.name
+        pdir = p.mode.lower()
+        bus = extractor.is_array(p.data_type)
+
+        # Convert Verilog modes
+        if pdir == "input":
+            pdir = "in"
+        elif pdir == "output":
+            pdir = "out"
+
+        # Determine which side the pin is on
+        if pdir in ("out", "inout"):
+            side = "r"
+        else:
+            side = "l"
+            assert pdir in ("in")
+
+        data_type = None
+        if not no_type:
+            if isinstance(p.data_type, VhdlParameterType):
+                data_type = p.data_type.name
+                if bus:
+                    sep = ":" if p.data_type.direction == "downto" else "\u2799"
+                    data_type = (
+                        f"{data_type}[{p.data_type.l_bound}{sep}{p.data_type.r_bound}]"
+                    )
+            else:
+                data_type = str(p.data_type)
+
+        pin = Pin(pname, side=side, data_type=data_type, bidir=pdir == "inout")
+
+        # Check for pin name patterns
+        pin_patterns = {
+            "clock": re.compile(r"(^cl(oc)?k)|(cl(oc)?k$)", re.IGNORECASE),
+            "bubble": re.compile(r"_[nb]$", re.IGNORECASE),
+            "bus": re.compile(r"(\[.*\]$)", re.IGNORECASE),
+        }
+
+        if pdir == "in" and pin_patterns["clock"].search(pname):
+            pin.clocked = True
+
+        if pin_patterns["bubble"].search(pname):
+            pin.bubble = True
+
+        if bus or pin_patterns["bus"].search(pname):
+            pin.bus = True
+
+        sect.add_pin(pin)
+
+    return sect
+
+
+def make_symbol(comp, extractor, title=False, libname=None, no_type=False):
+    """Create a symbol from a parsed component/module"""
+    vsym = HdlSymbol(comp.name if title else None, libname)
+    color_seq = sinebow.distinct_color_sequence(0.6)
+
+    if len(comp.generics) > 0:  # 'generic' in entity_data:
+        s = make_section(None, comp.generics, (200, 200, 200), extractor, no_type)
+        s.line_color = (100, 100, 100)
+        gsym = Symbol([s], line_color=(100, 100, 100))
+        vsym.add_symbol(gsym)
+    if len(comp.ports) > 0:  # 'port' in entity_data:
+        psym = Symbol()
+
+        # Break ports into sections
         cur_sect = []
-        sect_name = comp.sections[i]
-      cur_sect.append(p)
-
-    if len(cur_sect) > 0:
-      sections.append((sect_name, cur_sect))
+        sections = []
+        sect_name = comp.sections[0] if 0 in comp.sections else None
+        for i, p in enumerate(comp.ports):
+            # Finish previous section
+            if i in comp.sections and len(cur_sect) > 0:
+                sections.append((sect_name, cur_sect))
+                cur_sect = []
+                sect_name = comp.sections[i]
+            cur_sect.append(p)
+
+        if len(cur_sect) > 0:
+            sections.append((sect_name, cur_sect))
+
+        for sdata in sections:
+            s = make_section(
+                sdata[0],
+                sdata[1],
+                sinebow.lighten(next(color_seq), 0.75),
+                extractor,
+                no_type,
+            )
+            psym.add_section(s)
+
+        vsym.add_symbol(psym)
+
+    return vsym
 
-    for sdata in sections:
-      s = make_section(sdata[0], sdata[1], sinebow.lighten(next(color_seq), 0.75), extractor, no_type)
-      psym.add_section(s)
-
-    vsym.add_symbol(psym)
-
-  return vsym
 
 def parse_args():
-  '''Parse command line arguments'''
-  parser = argparse.ArgumentParser(description='HDL symbol generator')
-  parser.add_argument('-i', '--input', dest='input', action='store', help='HDL source ("-" for STDIN)')
-  parser.add_argument('-o', '--output', dest='output', action='store', help='Output file')
-  parser.add_argument('--output-as-filename', dest='output_as_filename', action='store_true', help='The --output flag will be used directly as output filename')
-  parser.add_argument('-f', '--format', dest='format', action='store', default='svg', help='Output format')
-  parser.add_argument('-L', '--library', dest='lib_dirs', action='append',
-    default=['.'], help='Library path')
-  parser.add_argument('-s', '--save-lib', dest='save_lib', action='store', help='Save type def cache file')
-  parser.add_argument('-t', '--transparent', dest='transparent', action='store_true',
-    default=False, help='Transparent background')
-  parser.add_argument('--scale', dest='scale', action='store', default='1', help='Scale image')
-  parser.add_argument('--title', dest='title', action='store_true', default=False, help='Add component name above symbol')
-  parser.add_argument('--no-type', dest='no_type', action='store_true', default=False, help='Omit pin type information')
-  parser.add_argument('-v', '--version', dest='version', action='store_true', default=False, help='Symbolator version')
-  parser.add_argument('--libname', dest='libname', action='store', default='', help='Add libname above cellname, and move component name to bottom. Works only with --title')
-
-  args, unparsed = parser.parse_known_args()
-
-  if args.version:
-    print('Symbolator {}'.format(__version__))
-    sys.exit(0)
-
-  # Allow file to be passed in without -i
-  if args.input is None and len(unparsed) > 0:
-    args.input = unparsed[0]
-
-  if args.format.lower() in ('png', 'svg', 'pdf', 'ps', 'eps'):
-    args.format = args.format.lower()
-
-  if args.input == '-' and args.output is None: # Reading from stdin: must have full output file name
-    print('Error: Output file is required when reading from stdin')
-    sys.exit(1)
-
-  if args.libname != '' and not args.title:
-    print("Error: '--tile' is required when using libname")
-    sys.exit(1)
-
-  args.scale = float(args.scale)
-
-  # Remove duplicates
-  args.lib_dirs = list(set(args.lib_dirs))
-
-  return args
-
-
-def is_verilog_code(code):
-  '''Identify Verilog from stdin'''
-  return re.search('endmodule', code) is not None
-
-
-def file_search(base_dir, extensions=('.vhdl', '.vhd')):
-  '''Recursively search for files with matching extensions'''
-  extensions = set(extensions)
-  hdl_files = []
-  for root, dirs, files in os.walk(base_dir):
-    for f in files:
-      if os.path.splitext(f)[1].lower() in extensions:
-        hdl_files.append(os.path.join(root, f))
-
-  return hdl_files
-
-def create_directories(fname):
-  '''Create all parent directories in a file path'''
-  try:
-    os.makedirs(os.path.dirname(fname))
-  except OSError as e:
-    if e.errno != errno.EEXIST and e.errno != errno.ENOENT:
-      raise
-
-def reformat_array_params(vo):
-  '''Convert array ranges to Verilog style'''
-  for p in vo.ports:
-    # Replace VHDL downto and to
-    data_type = p.data_type.replace(' downto ', ':').replace(' to ', '\u2799')
-    # Convert to Verilog style array syntax
-    data_type = re.sub(r'([^(]+)\((.*)\)$', r'\1[\2]', data_type)
-
-    # Split any array segment
-    pieces = data_type.split('[')
-    if len(pieces) > 1:
-      # Strip all white space from array portion
-      data_type = '['.join([pieces[0], pieces[1].replace(' ', '')])
-
-    p.data_type = data_type
-
-def main():
-  '''Run symbolator'''
-  args = parse_args()
-
-  style = DrawStyle()
-  style.line_color = (0,0,0)
-
-  vhdl_ex = vhdl.VhdlExtractor()
-  vlog_ex = vlog.VerilogExtractor()
-
-  if os.path.isfile(args.lib_dirs[0]):
-    # This is a file containing previously parsed array type names
-    vhdl_ex.load_array_types(args.lib_dirs[0])
-
-  else: # args.lib_dirs is a path
-    # Find all library files
-    flist = []
-    for lib in args.lib_dirs:
-      print('Scanning library:', lib)
-      flist.extend(file_search(lib, extensions=('.vhdl', '.vhd', '.vlog', '.v'))) # Get VHDL and Verilog files
-    if args.input and os.path.isfile(args.input):
-      flist.append(args.input)
+    """Parse command line arguments"""
+    parser = argparse.ArgumentParser(description="HDL symbol generator")
+    parser.add_argument(
+        "-i", "--input", dest="input", action="store", help='HDL source ("-" for STDIN)'
+    )
+    parser.add_argument(
+        "-o", "--output", dest="output", action="store", help="Output file"
+    )
+    parser.add_argument(
+        "--output-as-filename",
+        dest="output_as_filename",
+        action="store_true",
+        help="The --output flag will be used directly as output filename",
+    )
+    parser.add_argument(
+        "-f",
+        "--format",
+        dest="format",
+        action="store",
+        default="svg",
+        help="Output format",
+    )
+    parser.add_argument(
+        "-L",
+        "--library",
+        dest="lib_dirs",
+        action="append",
+        default=["."],
+        help="Library path",
+    )
+    parser.add_argument(
+        "-s",
+        "--save-lib",
+        dest="save_lib",
+        action="store_true",
+        default=False,
+        help="Save type def cache file",
+    )
+    parser.add_argument(
+        "-t",
+        "--transparent",
+        dest="transparent",
+        action="store_true",
+        default=False,
+        help="Transparent background",
+    )
+    parser.add_argument(
+        "--scale",
+        dest="scale",
+        action="store",
+        default=1.0,
+        type=float,
+        help="Scale image",
+    )
+    parser.add_argument(
+        "--title",
+        dest="title",
+        action="store_true",
+        default=False,
+        help="Add component name above symbol",
+    )
+    parser.add_argument(
+        "--no-type",
+        dest="no_type",
+        action="store_true",
+        default=False,
+        help="Omit pin type information",
+    )
+    parser.add_argument(
+        "-v",
+        "--version",
+        action="version",
+        version=f"%(prog)s {__version__}",
+        help="Print symbolator version and exit",
+    )
+    parser.add_argument(
+        "--libname",
+        dest="libname",
+        action="store",
+        default="",
+        help="Add libname above cellname, and move component name to bottom. Works only with --title",
+    )
+    parser.add_argument(
+        "--debug",
+        action="store_const",
+        dest="loglevel",
+        const=logging.DEBUG,
+        default=logging.INFO,
+        help="Print debug messages.",
+    )
+
+    args, unparsed = parser.parse_known_args()
+    logging.basicConfig(level=args.loglevel)
+
+    # Allow file to be passed in without -i
+    if args.input is None and len(unparsed) > 0:
+        args.input = unparsed[0]
+
+    if args.format.lower() in ("png", "svg", "pdf", "ps", "eps"):
+        args.format = args.format.lower()
+
+    if (
+        args.input == "-" and args.output is None
+    ):  # Reading from stdin: must have full output file name
+        log.critical("Error: Output file is required when reading from stdin")
+        sys.exit(1)
+
+    if args.libname != "" and not args.title:
+        log.critical("Error: '--title' is required when using libname")
+        sys.exit(1)
+
+    # Remove duplicates
+    args.lib_dirs = list(set(args.lib_dirs))
+
+    return args
+
+
+def file_search(base_dir, extensions=(".vhdl", ".vhd")):
+    """Recursively search for files with matching extensions"""
+    extensions = set(extensions)
+    hdl_files = []
+    for root, dirs, files in os.walk(base_dir):
+        for f in files:
+            if os.path.splitext(f)[1].lower() in extensions:
+                hdl_files.append(os.path.join(root, f))
+
+    return hdl_files
+
+
+def filter_types(objects: Iterator[Any], types: List[Type]):
+    """keep only objects which are instances of _any_ of the types in 'types'"""
+    return filter(lambda o: any(map(lambda clz: isinstance(o, clz), types)), objects)
 
-    # Find all of the array types
-    vhdl_ex.register_array_types_from_sources(flist)
 
-    #print('## ARRAYS:', vhdl_ex.array_types)
-
-  if args.save_lib:
-    print('Saving type defs to "{}".'.format(args.save_lib))
-    vhdl_ex.save_array_types(args.save_lib)
-
-
-  if args.input is None:
-    print("Error: Please provide a proper input file")
-    sys.exit(0)
-
-  if args.input == '-': # Read from stdin
-    code = ''.join(list(sys.stdin))
-    if is_verilog_code(code):
-      all_components = {'': [(c, vlog_ex) for c in vlog_ex.extract_objects_from_source(code)]}
-    else:
-      all_components = {'': [(c, vhdl_ex) for c in vhdl_ex.extract_objects_from_source(code, VhdlComponent)]}
-    # Output is a named file
-
-  elif os.path.isfile(args.input):
-    if vhdl.is_vhdl(args.input):
-      all_components = {args.input: [(c, vhdl_ex) for c in vhdl_ex.extract_objects(args.input, VhdlComponent)]}
+def main():
+    """Run symbolator"""
+    args = parse_args()
+
+    style = DrawStyle()
+    style.line_color = (0, 0, 0)
+
+    vhdl_ex = vhdl.VhdlExtractor()
+    vlog_ex = vlog.VerilogExtractor()
+
+    if os.path.isfile(args.lib_dirs[0]):
+        # This is a file containing previously parsed array type names
+        vhdl_ex.load_array_types(args.lib_dirs[0])
+
+    else:  # args.lib_dirs is a path
+        # Find all library files
+        flist = []
+        for lib in args.lib_dirs:
+            log.info(f"Scanning library: {lib}")
+            # Get VHDL and Verilog files
+            flist.extend(file_search(lib, extensions=(".vhdl", ".vhd", ".vlog", ".v")))
+        if args.input and os.path.isfile(args.input):
+            flist.append(args.input)
+
+        log.debug(f"Finding array type from following sources: {flist}")
+        # Find all of the array types
+        vhdl_ex.register_array_types_from_sources(flist)
+        log.debug(f"Discovered VHDL array types: {vhdl_ex.array_types}")
+
+    if args.save_lib:
+        log.info(f'Saving type defs to "{args.save_lib}"')
+        vhdl_ex.save_array_types(args.save_lib)
+
+    if not args.input:
+        log.critical("Error: Please provide a proper input file")
+        sys.exit(0)
+
+    log.debug(f"args.input={args.input}")
+
+    vhdl_types = [VhdlComponent, VhdlEntity]
+
+    if args.input == "-":  # Read from stdin
+        code = "".join(list(sys.stdin))
+        vlog_objs = vlog_ex.extract_objects_from_source(code)
+
+        all_components = {
+            "": (vlog_ex, vlog_objs)
+            if vlog_objs
+            else (
+                vhdl_ex,
+                filter_types(vhdl_ex.extract_objects_from_source(code), vhdl_types),
+            )
+        }
     else:
-      all_components = {args.input: [(c, vlog_ex) for c in vlog_ex.extract_objects(args.input)]}
-    # Output is a directory
-
-  elif os.path.isdir(args.input):
-    flist = set(file_search(args.input, extensions=('.vhdl', '.vhd', '.vlog', '.v')))
-
-    # Separate file by extension
-    vhdl_files = set(f for f in flist if vhdl.is_vhdl(f))
-    vlog_files = flist - vhdl_files
-
-    all_components = {f: [(c, vhdl_ex) for c in vhdl_ex.extract_objects(f, VhdlComponent)] for f in vhdl_files}
-
-    vlog_components = {f: [(c, vlog_ex) for c in vlog_ex.extract_objects(f)] for f in vlog_files}
-    all_components.update(vlog_components)
-    # Output is a directory
-
-  else:
-    print('Error: Invalid input source')
-    sys.exit(1)
-
-  if args.output:
-    create_directories(args.output)
-
-  nc = NuCanvas(None)
-
-  # Set markers for all shapes
-  nc.add_marker('arrow_fwd',
-    PathShape(((0,-4), (2,-1, 2,1, 0,4), (8,0), 'z'), fill=(0,0,0), weight=0),
-    (3.2,0), 'auto', None)
-
-  nc.add_marker('arrow_back',
-    PathShape(((0,-4), (-2,-1, -2,1, 0,4), (-8,0), 'z'), fill=(0,0,0), weight=0),
-    (-3.2,0), 'auto', None)
-
-  nc.add_marker('bubble',
-    OvalShape(-3,-3, 3,3, fill=(255,255,255), weight=1),
-    (0,0), 'auto', None)
-
-  nc.add_marker('clock',
-    PathShape(((0,-7), (0,7), (7,0), 'z'), fill=(255,255,255), weight=1),
-    (0,0), 'auto', None)
-
-  # Render every component from every file into an image
-  for source, components in all_components.items():
-    for comp, extractor in components:
-      comp.name = comp.name.strip('_')
-      reformat_array_params(comp)
-      if source == '' or args.output_as_filename:
-        fname = args.output
-      else:
-        fname = '{}{}.{}'.format(
-            args.libname + "__" if args.libname is not None or args.libname != "" else "",
-            comp.name,
-            args.format)
-        if args.output:
-          fname = os.path.join(args.output, fname)
-      print('Creating symbol for {} "{}"\n\t-> {}'.format(source, comp.name, fname))
-      if args.format == 'svg':
-        surf = SvgSurface(fname, style, padding=5, scale=args.scale)
-      else:
-        surf = CairoSurface(fname, style, padding=5, scale=args.scale)
-
-      nc.set_surface(surf)
-      nc.clear_shapes()
-
-      sym = make_symbol(comp, extractor, args.title, args.libname, args.no_type)
-      sym.draw(0,0, nc)
-
-      nc.render(args.transparent)
-
-if __name__ == '__main__':
-  main()
+        if os.path.isfile(args.input):
+            flist = [args.input]
+        elif os.path.isdir(args.input):
+            flist = file_search(args.input, extensions=(".vhdl", ".vhd", ".vlog", ".v"))
+        else:
+            log.critical("Error: Invalid input source")
+            sys.exit(1)
+
+        all_components = dict()
+        for f in flist:
+            if vhdl.is_vhdl(f):
+                all_components[f] = (vhdl_ex, vhdl_filter(vhdl_ex.extract_objects(f)))
+            else:
+                all_components[f] = (vlog_ex, vlog_ex.extract_objects(f))
+
+    log.debug(f"all_components={all_components}")
+
+    if args.output:
+        os.makedirs(os.path.dirname(args.output), exist_ok=True)
+
+    nc = NuCanvas(None)
+
+    # Set markers for all shapes
+    nc.add_marker(
+        "arrow_fwd",
+        PathShape(
+            ((0, -4), (2, -1, 2, 1, 0, 4), (8, 0), "z"), fill=(0, 0, 0), weight=0
+        ),
+        (3.2, 0),
+        "auto",
+        None,
+    )
+
+    nc.add_marker(
+        "arrow_back",
+        PathShape(
+            ((0, -4), (-2, -1, -2, 1, 0, 4), (-8, 0), "z"), fill=(0, 0, 0), weight=0
+        ),
+        (-3.2, 0),
+        "auto",
+        None,
+    )
+
+    nc.add_marker(
+        "bubble",
+        OvalShape(-3, -3, 3, 3, fill=(255, 255, 255), weight=1),
+        (0, 0),
+        "auto",
+        None,
+    )
+
+    nc.add_marker(
+        "clock",
+        PathShape(((0, -7), (0, 7), (7, 0), "z"), fill=(255, 255, 255), weight=1),
+        (0, 0),
+        "auto",
+        None,
+    )
+
+    # Render every component from every file into an image
+    for source, (extractor, components) in all_components.items():
+        for comp in components:
+            log.debug(f"source: {source} component: {comp}")
+            comp.name = comp.name.strip("_")
+            if source == "" or args.output_as_filename:
+                fname = args.output
+            else:
+                fname = f'{args.libname + "__" if args.libname else ""}{comp.name}.{args.format}'
+                if args.output:
+                    fname = os.path.join(args.output, fname)
+            log.info(
+                'Creating symbol for {} "{}"\n\t-> {}'.format(source, comp.name, fname)
+            )
+            if args.format == "svg":
+                surf = SvgSurface(fname, style, padding=5, scale=args.scale)
+            else:
+                surf = CairoSurface(fname, style, padding=5, scale=args.scale)
+
+            nc.set_surface(surf)
+            nc.clear_shapes()
+
+            sym = make_symbol(comp, extractor, args.title, args.libname, args.no_type)
+            sym.draw(0, 0, nc)
+
+            nc.render(args.transparent)
+
+
+if __name__ == "__main__":
+    main()
+
+
+def test_is_verilog():
+    positive = [
+        """\
+            module M
+            endmodule""",
+        """
+            module Mod1(A, B, C);
+              input A, B;
+              output C;
+              assign C = A & B;
+            endmodule
+        """,
+    ]
+    negative = [
+        """\
+            entity mymodule is -- my module
+            end mymodule;""",
+        """
+            entity sendmodule is -- the sending module
+            end sendmodule;
+        """,
+    ]
+    vlog_ex = vlog.VerilogExtractor()
+
+    def is_verilog_code(code):
+        vlog_objs = vlog_ex.extract_objects_from_source(code)
+        print(vlog_objs)
+        return len(vlog_objs) > 0
+
+    for code in positive:
+        code = textwrap.dedent(code)
+        assert is_verilog_code(code)
+    for code in negative:
+        code = textwrap.dedent(code)
+        assert not is_verilog_code(code)
diff --git a/symbolator_sphinx/symbolator_sphinx.py b/symbolator_sphinx/symbolator_sphinx.py
index 4e50c50..7cd605d 100644
--- a/symbolator_sphinx/symbolator_sphinx.py
+++ b/symbolator_sphinx/symbolator_sphinx.py
@@ -12,21 +12,19 @@
     :license: BSD, see LICENSE.Sphinx for details.
 """
 
-import re
 import codecs
 import posixpath
 from errno import ENOENT, EPIPE, EINVAL
 from os import path
 from subprocess import Popen, PIPE
 from hashlib import sha1
-
-from six import text_type
+from typing import Any, Dict, List, Tuple, Optional
 
 from docutils import nodes
 from docutils.parsers.rst import Directive, directives
 from docutils.statemachine import ViewList
 
-import sphinx
+from sphinx.application import Sphinx
 from sphinx.errors import SphinxError
 from sphinx.locale import _, __
 from sphinx.util import logging
@@ -50,8 +48,7 @@ class symbolator(nodes.General, nodes.Inline, nodes.Element):
     pass
 
 
-def figure_wrapper(directive, node, caption):
-    # type: (Directive, nodes.Node, unicode) -> nodes.figure
+def figure_wrapper(directive: Directive, node: symbolator, caption: str):
     figure_node = nodes.figure('', node)
     if 'align' in node:
         figure_node['align'] = node.attributes.pop('align')
@@ -67,8 +64,7 @@ def figure_wrapper(directive, node, caption):
     return figure_node
 
 
-def align_spec(argument):
-    # type: (Any) -> bool
+def align_spec(argument) -> bool:
     return directives.choice(argument, ('left', 'center', 'right'))
 
 
@@ -88,14 +84,13 @@ class Symbolator(Directive):
         'name': directives.unchanged,
     }
 
-    def run(self):
-        # type: () -> List[nodes.Node]
+    def run(self) -> List[nodes.Node]:
         if self.arguments:
             document = self.state.document
             if self.content:
                 return [document.reporter.warning(
                     __('Symbolator directive cannot have both content and '
-                    'a filename argument'), line=self.lineno)]
+                       'a filename argument'), line=self.lineno)]
             env = self.state.document.settings.env
             argument = search_image_for_language(self.arguments[0], env)
             rel_filename, filename = env.relfn2path(argument)
@@ -106,7 +101,7 @@ def run(self):
             except (IOError, OSError):
                 return [document.reporter.warning(
                     __('External Symbolator file %r not found or reading '
-                    'it failed') % filename, line=self.lineno)]
+                       'it failed') % filename, line=self.lineno)]
         else:
             symbolator_code = '\n'.join(self.content)
             if not symbolator_code.strip():
@@ -124,7 +119,7 @@ def run(self):
             node['align'] = self.options['align']
 
         if 'name' in self.options:
-          node['options']['name'] = self.options['name']
+            node['options']['name'] = self.options['name']
 
         caption = self.options.get('caption')
         if caption:
@@ -134,9 +129,7 @@ def run(self):
         return [node]
 
 
-
-def render_symbol(self, code, options, format, prefix='symbol'):
-    # type: (nodes.NodeVisitor, unicode, Dict, unicode, unicode) -> Tuple[unicode, unicode]
+def render_symbol(self, code: str, options: Dict[str, Any], format: str, prefix: str = 'symbol') -> Tuple[Optional[str], Optional[str]]:
     """Render symbolator code into a PNG or SVG output file."""
 
     symbolator_cmd = options.get('symbolator_cmd', self.builder.config.symbolator_cmd)
@@ -159,15 +152,38 @@ def render_symbol(self, code, options, format, prefix='symbol'):
     ensuredir(path.dirname(outfn))
 
     # Symbolator expects UTF-8 by default
-    if isinstance(code, text_type):
-        code = code.encode('utf-8')
+    assert isinstance(code, str)
+    code_bytes: bytes = code.encode('utf-8')
 
     cmd_args = [symbolator_cmd]
     cmd_args.extend(self.builder.config.symbolator_cmd_args)
     cmd_args.extend(['-i', '-', '-f', format, '-o', outfn])
 
     try:
-        p = Popen(cmd_args, stdout=PIPE, stdin=PIPE, stderr=PIPE)
+        with Popen(cmd_args, stdout=PIPE, stdin=PIPE, stderr=PIPE) as p:
+            try:
+                # Symbolator may close standard input when an error occurs,
+                # resulting in a broken pipe on communicate()
+                stdout, stderr = p.communicate(code_bytes)
+            except (OSError, IOError) as err:
+                if err.errno not in (EPIPE, EINVAL):
+                    raise
+                # in this case, read the standard output and standard error streams
+                # directly, to get the error message(s)
+                if p.stdout and p.stderr:
+                    stdout, stderr = p.stdout.read(), p.stderr.read()
+                    p.wait()
+                else:
+                    stdout, stderr = None, None
+            if stdout and stderr:
+                stdout_str, stderr_str = stdout.decode('utf-8'), stderr.decode('utf-8')
+                if p.returncode != 0:
+                    raise SymbolatorError(f'symbolator exited with error:\n[stderr]\n{stderr_str}\n'
+                                          f'[stdout]\n{stdout_str}')
+                if not path.isfile(outfn):
+                    raise SymbolatorError(f'symbolator did not produce an output file:\n[stderr]\n{stderr_str}\n'
+                                          f'[stdout]\n{stdout_str}')
+            return relfn, outfn
     except OSError as err:
         if err.errno != ENOENT:   # No such file or directory
             raise
@@ -177,34 +193,15 @@ def render_symbol(self, code, options, format, prefix='symbol'):
             self.builder._symbolator_warned_cmd = {}
         self.builder._symbolator_warned_cmd[symbolator_cmd] = True
         return None, None
-    try:
-        # Symbolator may close standard input when an error occurs,
-        # resulting in a broken pipe on communicate()
-        stdout, stderr = p.communicate(code)
-    except (OSError, IOError) as err:
-        if err.errno not in (EPIPE, EINVAL):
-            raise
-        # in this case, read the standard output and standard error streams
-        # directly, to get the error message(s)
-        stdout, stderr = p.stdout.read(), p.stderr.read()
-        p.wait()
-    if p.returncode != 0:
-        raise SymbolatorError('symbolator exited with error:\n[stderr]\n%s\n'
-                            '[stdout]\n%s' % (stderr, stdout))
-    if not path.isfile(outfn):
-        raise SymbolatorError('symbolator did not produce an output file:\n[stderr]\n%s\n'
-                            '[stdout]\n%s' % (stderr, stdout))
-    return relfn, outfn
-
-
-def render_symbol_html(self, node, code, options, prefix='symbol',
-                    imgcls=None, alt=None):
-    # type: (nodes.NodeVisitor, symbolator, unicode, Dict, unicode, unicode, unicode) -> Tuple[unicode, unicode]  # NOQA
+
+
+def render_symbol_html(self, node, code, options, prefix='symbol', imgcls=None, alt=None):
+    # type: (nodes.NodeVisitor, symbolator, str, Dict, str, str, str) -> Tuple[str, str]  # NOQA
     format = self.builder.config.symbolator_output_format
     try:
         if format not in ('png', 'svg'):
             raise SymbolatorError("symbolator_output_format must be one of 'png', "
-                                "'svg', but is %r" % format)
+                                  "'svg', but is %r" % format)
         fname, outfn = render_symbol(self, code, options, format, prefix)
     except SymbolatorError as exc:
         logger.warning('symbolator code %r: ' % code + str(exc))
@@ -238,7 +235,7 @@ def html_visit_symbolator(self, node):
 
 
 def render_symbol_latex(self, node, code, options, prefix='symbol'):
-    # type: (nodes.NodeVisitor, symbolator, unicode, Dict, unicode) -> None
+    # type: (nodes.NodeVisitor, symbolator, str, Dict, str) -> None
     try:
         fname, outfn = render_symbol(self, code, options, 'pdf', prefix)
     except SymbolatorError as exc:
@@ -252,7 +249,7 @@ def render_symbol_latex(self, node, code, options, prefix='symbol'):
         para_separator = '\n'
 
     if fname is not None:
-        post = None  # type: unicode
+        post: Optional[str] = None
         if not is_inline and 'align' in node:
             if node['align'] == 'left':
                 self.body.append('{')
@@ -274,7 +271,7 @@ def latex_visit_symbolator(self, node):
 
 
 def render_symbol_texinfo(self, node, code, options, prefix='symbol'):
-    # type: (nodes.NodeVisitor, symbolator, unicode, Dict, unicode) -> None
+    # type: (nodes.NodeVisitor, symbolator, str, Dict, str) -> None
     try:
         fname, outfn = render_symbol(self, code, options, 'png', prefix)
     except SymbolatorError as exc:
@@ -308,8 +305,7 @@ def man_visit_symbolator(self, node):
     raise nodes.SkipNode
 
 
-def setup(app):
-    # type: (Sphinx) -> Dict[unicode, Any]
+def setup(app: Sphinx) -> Dict[str, Any]:
     app.add_node(symbolator,
                  html=(html_visit_symbolator, None),
                  latex=(latex_visit_symbolator, None),