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),