From a269c4f917e0bcf427700f2d31a712c22f6e63af Mon Sep 17 00:00:00 2001 From: Kamyar Mohajerani Date: Tue, 8 Mar 2022 16:35:09 -0500 Subject: [PATCH 1/3] switch to setup.cfg + dep: hdlparse@master --- setup.cfg | 35 ++ setup.py | 61 +-- symbolator.py | 1076 +++++++++++++++++++++++++------------------------ 3 files changed, 596 insertions(+), 576 deletions(-) create mode 100644 setup.cfg mode change 100755 => 100644 setup.py diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..47a87f5 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,35 @@ +[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 = + hdlparse @ git+https://github.com/hdl/pyHDLParser.git +include_package_data = True + +[options.entry_points] +console_scripts = + symbolator = symbolator:main \ No newline at end of file diff --git a/setup.py b/setup.py old mode 100755 new mode 100644 index 4db9fd8..fc1f76c --- 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() \ No newline at end of file diff --git a/symbolator.py b/symbolator.py index 36470d8..f13d347 100755 --- a/symbolator.py +++ b/symbolator.py @@ -1,10 +1,14 @@ -#!/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 errno from nucanvas import DrawStyle, NuCanvas from nucanvas.cairo_backend import CairoSurface @@ -17,578 +21,616 @@ from hdlparse.vhdl_parser import VhdlComponent -__version__ = '1.0.2' +__version__ = '1.1.0' 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 + '''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'] - - @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 + '''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'] + + @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') + # Compute title offset + if self.show_name and self.name is not None and len(self.name) > 0: + 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)) - 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 = [] - - 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 + '''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) - # 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 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')) 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() + '''Create a section from a pin list''' + sect = PinSection(sname, fill=fill) + side = 'l' - # Convert Verilog modes - if pdir == 'input': - pdir = 'in' - if pdir == 'output': - pdir = 'out' + 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) - # Determine which side the pin is on - if pdir in ('in'): - side = 'l' - elif pdir in ('out', 'inout'): - side = 'r' + pdir = pdir.lower() - pin = Pin(pname, side=side, data_type=data_type) - if pdir == 'inout': - pin.bidir = True + # Convert Verilog modes + if pdir == 'input': + pdir = 'in' + if pdir == 'output': + pdir = 'out' - # 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) - } + # Determine which side the pin is on + if pdir in ('in'): + side = 'l' + elif pdir in ('out', 'inout'): + side = 'r' - if pdir == 'in' and pin_patterns['clock'].search(pname): - pin.clocked = True + pin = Pin(pname, side=side, data_type=data_type) + if pdir == 'inout': + pin.bidir = True - if pin_patterns['bubble'].search(pname): - pin.bubble = 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 bus or pin_patterns['bus'].search(pname): - pin.bus = True + if pdir == 'in' and pin_patterns['clock'].search(pname): + pin.clocked = True - sect.add_pin(pin) + if pin_patterns['bubble'].search(pname): + pin.bubble = True - return sect + if bus or pin_patterns['bus'].search(pname): + pin.bus = True -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)) - cur_sect = [] - sect_name = comp.sections[i] - cur_sect.append(p) + sect.add_pin(pin) - if len(cur_sect) > 0: - sections.append((sect_name, cur_sect)) + return 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 - -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') +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() - args, unparsed = parser.parse_known_args() + color_seq = sinebow.distinct_color_sequence(0.6) - if args.version: - print('Symbolator {}'.format(__version__)) - sys.exit(0) + 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() - # Allow file to be passed in without -i - if args.input is None and len(unparsed) > 0: - args.input = unparsed[0] + # 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): + # 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 args.format.lower() in ('png', 'svg', 'pdf', 'ps', 'eps'): - args.format = args.format.lower() + if len(cur_sect) > 0: + sections.append((sect_name, cur_sect)) - 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) + 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) - if args.libname != '' and not args.title: - print("Error: '--tile' is required when using libname") - sys.exit(1) + vsym.add_symbol(psym) - args.scale = float(args.scale) + return vsym - # Remove duplicates - args.lib_dirs = list(set(args.lib_dirs)) - return args +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 + '''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)) + '''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 + 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]) +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 - 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) - # Find all of the array types - vhdl_ex.register_array_types_from_sources(flist) +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) - #print('## ARRAYS:', vhdl_ex.array_types) + # 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(' ', '')]) - if args.save_lib: - print('Saving type defs to "{}".'.format(args.save_lib)) - vhdl_ex.save_array_types(args.save_lib) + p.data_type = data_type - if args.input is None: - print("Error: Please provide a proper input file") - sys.exit(0) +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) + # 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) + + # 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)]} + 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 - 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 + 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) - 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)]} - 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() + main() From 279637bc70f09cfa285e4463762cc8384c1cf34f Mon Sep 17 00:00:00 2001 From: Kamyar Mohajerani Date: Wed, 9 Mar 2022 16:35:00 -0500 Subject: [PATCH 2/3] fix vhdl_parser if no component is defined - collect entities as well as components - cleanup and simplify code --- symbolator.py | 298 +++++++++++++++++++++++++------------------------- 1 file changed, 148 insertions(+), 150 deletions(-) diff --git a/symbolator.py b/symbolator.py index f13d347..a54529d 100755 --- a/symbolator.py +++ b/symbolator.py @@ -3,12 +3,13 @@ # Copyright © 2017 Kevin Thibedeau # Distributed under the terms of the MIT license - import sys import re import argparse import os -import errno +import logging +import textwrap +from typing import Any, Iterator, List, Type from nucanvas import DrawStyle, NuCanvas from nucanvas.cairo_backend import CairoSurface @@ -19,10 +20,12 @@ 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' +log = logging.getLogger(__name__) + def xml_escape(txt): '''Replace special characters for XML strings''' @@ -97,14 +100,14 @@ def draw(self, x, y, c): 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', + 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', + g.create_text(xs + self.padding, 0, anchor='w', text=self.styled_type, text_color=(150, 150, 150)) return g @@ -118,19 +121,19 @@ def text_width(self, c, font_params): class PinSection(object): '''Symbol section''' - def __init__(self, name, fill=None, line_color=(0, 0, 0)): + def __init__(self, name, fill=None, line_color=(0, 0, 0), title_font=('Verdana', 9, 'bold')): 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 name is not None: - m = re.match(r'^(\w+)\s*\|(.*)$', name) + m = re.match(r'^(\S+)\s*\|(.*)$', name) if m: self.name = m.group(2).strip() self.sect_class = m.group(1).strip().lower() @@ -140,7 +143,7 @@ def __init__(self, name, fill=None, line_color=(0, 0, 0)): 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 + 'control': sinebow.lighten(sinebow.sinebow(0.15), 0.75), # Yellow 'power': sinebow.lighten(sinebow.sinebow(0.07), 0.75) # Orange } @@ -192,19 +195,18 @@ def draw(self, x, y, width, c): toff = 0 - title_font = ('Times', 12, 'italic') # Compute title offset - if self.show_name and self.name is not None and len(self.name) > 0: - x0, y0, x1, y1, baseline = c.surf.text_bbox(self.name, title_font) + 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 + 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) + 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 @@ -218,7 +220,7 @@ def draw(self, x, y, width, c): p.draw(0 + width, toff + py, g) py += dy - return (g, (x, y+top, x+width, y+bot)) + return (g, (x, y + top, x + width, y + bot)) class Symbol(object): @@ -260,8 +262,7 @@ def draw(self, x, y, c, sym_width=None): 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) + c.create_rectangle(x0, y0, x1, y1, weight=self.line_weight, line_color=self.line_color) return (x0, y0, x1, y1) @@ -281,8 +282,7 @@ def add_symbol(self, 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 = 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 @@ -291,47 +291,57 @@ def draw(self, x, y, c): 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', + 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', + 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', + 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 + pdir = p.mode.lower() bus = extractor.is_array(p.data_type) - pdir = pdir.lower() - # Convert Verilog modes if pdir == 'input': pdir = 'in' - if pdir == 'output': + elif pdir == 'output': pdir = 'out' # Determine which side the pin is on - if pdir in ('in'): - side = 'l' - elif pdir in ('out', 'inout'): + 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) - if pdir == 'inout': - pin.bidir = True + pin = Pin( + pname, + side=side, + data_type=data_type, + bidir=pdir == 'inout' + ) # Check for pin name patterns pin_patterns = { @@ -354,15 +364,9 @@ def make_section(sname, sect_pins, fill, extractor, no_type=False): return sect -def make_symbol(comp, extractor, title=False, libname="", no_type=False): +def make_symbol(comp, extractor, title=False, libname=None, 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() - + 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: @@ -390,8 +394,7 @@ def make_symbol(comp, extractor, title=False, libname="", no_type=False): 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) + 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) @@ -402,10 +405,8 @@ def make_symbol(comp, extractor, title=False, libname="", no_type=False): 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('-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', @@ -413,25 +414,24 @@ def parse_args(): 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') + 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', help='Scale image') + 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', dest='version', - action='store_true', default=False, help='Symbolator version') + 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() - - if args.version: - print('Symbolator {}'.format(__version__)) - sys.exit(0) + logging.basicConfig(level=args.loglevel) # Allow file to be passed in without -i if args.input is None and len(unparsed) > 0: @@ -441,26 +441,19 @@ def parse_args(): 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') + log.critical('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") + log.critical("Error: '--title' 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) @@ -472,33 +465,9 @@ def file_search(base_dir, extensions=('.vhdl', '.vhd')): 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 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) def main(): '''Run symbolator''' @@ -518,67 +487,64 @@ def main(): # Find all library files flist = [] for lib in args.lib_dirs: - print('Scanning library:', lib) + log.info(f'Scanning library: {lib}') # Get VHDL and Verilog files - flist.extend(file_search(lib, extensions=( - '.vhdl', '.vhd', '.vlog', '.v'))) + 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) - - # print('## ARRAYS:', vhdl_ex.array_types) + log.debug(f"Discovered VHDL array types: {vhdl_ex.array_types}") if args.save_lib: - print('Saving type defs to "{}".'.format(args.save_lib)) + log.info(f'Saving type defs to "{args.save_lib}"') vhdl_ex.save_array_types(args.save_lib) - if args.input is None: - print("Error: Please provide a proper input file") + if not args.input: + log.critical("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)]} - 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 + log.debug(f"args.input={args.input}") - all_components = {f: [(c, vhdl_ex) for c in vhdl_ex.extract_objects( - f, VhdlComponent)] for f in vhdl_files} + vhdl_types = [VhdlComponent, VhdlEntity] - 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 + 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: - print('Error: Invalid input source') - sys.exit(1) + 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: - create_directories(args.output) + os.makedirs(os.path.dirname(args.output), exist_ok=True) nc = NuCanvas(None) @@ -603,20 +569,17 @@ def main(): (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: + for source, (extractor, components) in all_components.items(): + for comp in components: + log.debug(f"source: {source} component: {comp}") 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) + fname = f'{args.libname + "__" if 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)) + 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: @@ -625,8 +588,7 @@ def main(): nc.set_surface(surf) nc.clear_shapes() - sym = make_symbol(comp, extractor, args.title, - args.libname, args.no_type) + sym = make_symbol(comp, extractor, args.title, args.libname, args.no_type) sym.draw(0, 0, nc) nc.render(args.transparent) @@ -634,3 +596,39 @@ def main(): 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) From 79294d28da4a11dd4327c5d0cb0eb1053a296f8c Mon Sep 17 00:00:00 2001 From: Kamyar Mohajerani Date: Wed, 2 Nov 2022 16:41:04 -0400 Subject: [PATCH 3/3] inline color description for signal sections --- nucanvas/nucanvas.py | 196 +++-- nucanvas/shapes.py | 996 ++++++++++++------------- nucanvas/svg_backend.py | 836 +++++++++++---------- setup.cfg | 9 +- setup.py | 2 +- symbolator.py | 491 +++++++----- symbolator_sphinx/symbolator_sphinx.py | 92 ++- 7 files changed, 1387 insertions(+), 1235 deletions(-) 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 render(self, canvas, transparent=False): - x0,y0,x1,y1 = canvas.bbox('all') - self.markers = canvas.markers + def render(self, canvas, transparent=False): + x0, y0, x1, y1 = canvas.bbox('all') + self.markers = canvas.markers - W = x1 - x0 + 2*self.padding - H = y1 - y0 + 2*self.padding + W = x1 - x0 + 2 * self.padding + H = y1 - y0 + 2 * self.padding - x0 = int(x0) - y0 = int(y0) + x0 = int(x0) + y0 = int(y0) - # Reposition all shapes in the viewport + # Reposition all shapes in the viewport # for s in canvas.shapes: # s.move(-x0 + self.padding, -y0 + self.padding) - vbox = ' '.join(str(s) for s in (x0-self.padding,y0-self.padding, W,H)) + vbox = ' '.join(str(s) for s in (x0 - self.padding, y0 - self.padding, W, H)) - # Generate CSS for fonts - text_color = rgb_to_hex(self.def_styles.text_color) + # Generate CSS for fonts + text_color = rgb_to_hex(self.def_styles.text_color) - # Get fonts from all shapes - class FontVisitor(object): - def __init__(self): - self.font_ix = 1 - self.font_set = {} + # Get fonts from all shapes + class FontVisitor(object): + def __init__(self): + self.font_ix = 1 + self.font_set = {} - def get_font_info(self, s): - if 'font' in s.options: - fdef = s.options['font'] - fdata = (fdef,(0,0,0)) + def get_font_info(self, s): + if 'font' in s.options: + fdef = s.options['font'] + fdata = (fdef, (0, 0, 0)) - if fdata not in self.font_set: - self.font_set[fdata] = 'fnt' + str(self.font_ix) - self.font_ix += 1 - #print('# FONT:', s.options['font']) + if fdata not in self.font_set: + self.font_set[fdata] = 'fnt' + str(self.font_ix) + self.font_ix += 1 + # print('# FONT:', s.options['font']) - fclass = self.font_set[fdata] - s.options['css_class'] = fclass + fclass = self.font_set[fdata] + s.options['css_class'] = fclass - fv = FontVisitor() - visit_shapes(canvas, fv.get_font_info) + fv = FontVisitor() + visit_shapes(canvas, fv.get_font_info) - #print('## FSET:', fv.font_set) + # print('## FSET:', fv.font_set) - font_css = [] - for fs, fid in fv.font_set.items(): - family, size, weight = fs[0] - text_color = rgb_to_hex(fs[1]) + font_css = [] + for fs, fid in fv.font_set.items(): + family, size, weight = fs[0] + text_color = rgb_to_hex(fs[1]) - if weight == 'italic': - style = 'italic' - weight = 'normal' - else: - style = 'normal' + if weight == 'italic': + style = 'italic' + weight = 'normal' + else: + style = 'normal' - font_css.append('''.{} {{fill:{}; + font_css.append('''.{} {{fill:{}; font-family:{}; font-size:{}pt; font-weight:{}; font-style:{};}}'''.format(fid, - 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()]) + 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 index 47a87f5..6e650e2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,9 +27,14 @@ packages = symbolator_sphinx py_modules = symbolator install_requires = - hdlparse @ git+https://github.com/hdl/pyHDLParser.git + sphinx>=4.3,<5 + hdlparse @ git+https://github.com/kammoh/pyHDLParser.git include_package_data = True [options.entry_points] console_scripts = - symbolator = symbolator:main \ No newline at end of file + symbolator = symbolator:main + +[pycodestyle] +max_line_length = 120 +ignore = E501 diff --git a/setup.py b/setup.py index fc1f76c..6068493 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,3 @@ from setuptools import setup -setup() \ No newline at end of file +setup() diff --git a/symbolator.py b/symbolator.py index a54529d..bc2fd18 100755 --- a/symbolator.py +++ b/symbolator.py @@ -22,24 +22,33 @@ from hdlparse.vhdl_parser import VhdlComponent, VhdlEntity, VhdlParameterType -__version__ = '1.1.0' +__version__ = "1.1.0" 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('"', '"') + """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): + """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 @@ -54,28 +63,34 @@ def __init__(self, text, side='l', bubble=False, clocked=False, bus=False, bidir @property def styled_text(self): - return re.sub(r'(\[.*\])', r'\1', xml_escape(self.text)) + 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)) + 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 + # r = self.bubble_rad - if self.side == 'l': + if self.side == "l": xs = -self.pin_length - #bx = -r - #xe = 2*bx if self.bubble else 0 + # 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 + # bx = r + # xe = 2*bx if self.bubble else 0 xe = 0 # Whisker for pin @@ -83,32 +98,42 @@ def draw(self, x, y, c): 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 + 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 + # 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 + 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.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)) + 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) + 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)) + g.create_text( + xs + self.padding, + 0, + anchor="w", + text=self.styled_type, + text_color=(150, 150, 150), + ) return g @@ -118,10 +143,17 @@ def text_width(self, c, font_params): return self.padding + w -class PinSection(object): - '''Symbol section''' +class PinSection: + """Symbol section""" - def __init__(self, name, fill=None, line_color=(0, 0, 0), title_font=('Verdana', 9, 'bold')): + 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 @@ -132,34 +164,47 @@ def __init__(self, name, fill=None, line_color=(0, 0, 0), title_font=('Verdana', 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+)\s*\|(.*)$', name) + m = re.match(r"^([^\|]+)\s*(\|(\w*))?$", 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] + 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'] + 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'] + return [p for p in self.pins if p.side == "r"] @property def rows(self): @@ -202,8 +247,9 @@ def draw(self, x, y, width, c): 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) + 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) @@ -224,7 +270,7 @@ def draw(self, x, y, width, c): class Symbol(object): - '''Symbol composed of sections''' + """Symbol composed of sections""" def __init__(self, sections=None, line_color=(0, 0, 0)): if sections is not None: @@ -251,7 +297,7 @@ def draw(self, x, y, c, sym_width=None): bb = sg.bbox yoff += bb[3] - bb[1] sect_boxes.append(sb) - #section.draw(50, 100 + h, sym_width, nc) + # section.draw(50, 100 + h, sym_width, nc) # Find outline of all sections hw = self.line_weight / 2.0 - 0.5 @@ -262,15 +308,24 @@ def draw(self, x, y, c, sym_width=None): 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) + 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): + """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 @@ -282,7 +337,9 @@ def add_symbol(self, 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 = 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 @@ -291,21 +348,36 @@ def draw(self, x, y, c): 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')) + 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')) + 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')) + 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''' + """Create a section from a pin list""" sect = PinSection(sname, fill=fill) for p in sect_pins: @@ -314,49 +386,46 @@ def make_section(sname, sect_pins, fill, extractor, no_type=False): bus = extractor.is_array(p.data_type) # Convert Verilog modes - if pdir == 'input': - pdir = 'in' - elif pdir == 'output': - pdir = 'out' + if pdir == "input": + pdir = "in" + elif pdir == "output": + pdir = "out" # Determine which side the pin is on - if pdir in ('out', 'inout'): - side = 'r' + if pdir in ("out", "inout"): + side = "r" else: - side = 'l' - assert pdir in ('in') + 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}]" + 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' - ) + 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) + "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): + if pdir == "in" and pin_patterns["clock"].search(pname): pin.clocked = True - if pin_patterns['bubble'].search(pname): + if pin_patterns["bubble"].search(pname): pin.bubble = True - if bus or pin_patterns['bus'].search(pname): + if bus or pin_patterns["bus"].search(pname): pin.bus = True sect.add_pin(pin) @@ -365,13 +434,12 @@ def make_section(sname, sect_pins, fill, extractor, no_type=False): def make_symbol(comp, extractor, title=False, libname=None, no_type=False): - '''Create a symbol from a parsed component/module''' + """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 = 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) @@ -394,7 +462,13 @@ def make_symbol(comp, extractor, title=False, libname=None, no_type=False): 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) + 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) @@ -403,32 +477,96 @@ def make_symbol(comp, extractor, title=False, libname=None, no_type=False): 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_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.") + """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) @@ -437,14 +575,16 @@ def parse_args(): if args.input is None and len(unparsed) > 0: args.input = unparsed[0] - if args.format.lower() in ('png', 'svg', 'pdf', 'ps', 'eps'): + 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') + 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: + if args.libname != "" and not args.title: log.critical("Error: '--title' is required when using libname") sys.exit(1) @@ -454,8 +594,8 @@ def parse_args(): return args -def file_search(base_dir, extensions=('.vhdl', '.vhd')): - '''Recursively search for files with matching extensions''' +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): @@ -465,12 +605,14 @@ def file_search(base_dir, extensions=('.vhdl', '.vhd')): 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) + def main(): - '''Run symbolator''' + """Run symbolator""" args = parse_args() style = DrawStyle() @@ -487,9 +629,9 @@ def main(): # Find all library files flist = [] for lib in args.lib_dirs: - log.info(f'Scanning library: {lib}') + log.info(f"Scanning library: {lib}") # Get VHDL and Verilog files - flist.extend(file_search(lib, extensions=('.vhdl', '.vhd', '.vlog', '.v'))) + flist.extend(file_search(lib, extensions=(".vhdl", ".vhd", ".vlog", ".v"))) if args.input and os.path.isfile(args.input): flist.append(args.input) @@ -510,34 +652,31 @@ def main(): vhdl_types = [VhdlComponent, VhdlEntity] - if args.input == '-': # Read from stdin - code = ''.join(list(sys.stdin)) + 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)) + "": (vlog_ex, vlog_objs) + if vlog_objs + else ( + vhdl_ex, + filter_types(vhdl_ex.extract_objects_from_source(code), vhdl_types), + ) } else: 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') - ) + flist = file_search(args.input, extensions=(".vhdl", ".vhd", ".vlog", ".v")) else: - log.critical('Error: Invalid input source') + 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)) - ) + all_components[f] = (vhdl_ex, vhdl_filter(vhdl_ex.extract_objects(f))) else: all_components[f] = (vlog_ex, vlog_ex.extract_objects(f)) @@ -549,38 +688,57 @@ def main(): 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) + 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: + 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': + 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) @@ -594,7 +752,7 @@ def main(): nc.render(args.transparent) -if __name__ == '__main__': +if __name__ == "__main__": main() @@ -626,6 +784,7 @@ 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) 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),