# -*- coding: Latin-1 -*- """Convert graphviz graphs to LaTeX-friendly formats Various tools for converting graphs generated by the graphviz library to formats for use with LaTeX. Copyright (c) 2006-2009, Kjell Magne Fauske """ # Copyright (c) 2006-2009, Kjell Magne Fauske # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to # deal in the Software without restriction, including without limitation the # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or # sell copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. __author__ = 'Kjell Magne Fauske' __version__ = '2.8.7dev' __license__ = 'MIT' from itertools import izip from optparse import OptionParser import os.path as path import optparse import sys, tempfile, os, string,re import logging import tempfile from StringIO import StringIO from random import randint import warnings import dotparsing # Silence DeprecationWarnings about os.popen3 in Python 2.6 warnings.filterwarnings('ignore', category=DeprecationWarning, message=r'os\.popen3') # intitalize logging module log = logging.getLogger("dot2tex") console = logging.StreamHandler() console.setLevel(logging.WARNING) # set a format which is simpler for console use formatter = logging.Formatter('%(levelname)-8s %(message)s') # tell the handler to use this format console.setFormatter(formatter) log.addHandler(console) logstream = None loghandler = None DEFAULT_TEXTENCODING = 'utf8' DEFAULT_OUTPUT_FORMAT = 'pgf' # label margins in inches DEFAULT_LABEL_XMARGIN = 0.11 DEFAULT_LABEL_YMARGIN = 0.055 DEFAULT_EDGELABEL_XMARGIN = 0.01 DEFAULT_EDGELABEL_YMARGIN = 0.01 DEFAULT_GRAPHLABEL_XMARGIN = 0.01 DEFAULT_GRAPHLABEL_YMARGIN = 0.01 DEFAULT_NODE_WIDTH = 0.75 DEFAULT_NODE_HEIGHT = 0.5 # Todo: set papersize based on bb # Todo: Fontcolor # Todo: Support linewidth in draw string # Todo: Support linestyle in draw string # Todo: Need to reconsider edge draw order. # See for instance html2.xdot # Inch to bp conversion factor in2bp = 72.0 # Examples of draw strings # c 5 -black F 14.000000 11 -Times-Roman T 99 159 0 44 8 -a_1 test special_chars = ['$','\\','%','_','#','{',r'}','^','&'] special_chars_escape = [r'\$', r'$\backslash$',r'\%',r'\_',r'\#', r'\{',r'\}',r'\^{}',r'\&'] charmap = dict(zip(special_chars,special_chars_escape)) helpmsg = """\ Failed to parse the input data. Is it a valid dot file? Try to input xdot data directly. Example: dot -Txdot file.dot | dot2tex > file.tex If this does not work, check that you have an updated version of PyParsing and Graphviz. Users have reported problems with old versions. You can also run dot2tex in debug mode using the --debug option: dot2tex --debug file.dot A file dot2tex.log will be written to the current directory with detailed information useful for debugging.""" def mreplace(s, chararray, newchararray): for a, b in zip(chararray, newchararray): s = s.replace(a, b) return s def escape_texchars(string): r"""Escape the special LaTeX-chars %{}_^ Examples: >>> escape_texchars('10%') '10\\%' >>> escape_texchars('%{}_^\\$') '\\%\\{\\}\\_\\^{}$\\backslash$\\$' """ return "".join([charmap.get(c,c) for c in string]) def tikzify(s): if s.strip(): return mreplace(s,r'\,:.','-+_*') else: return "d2tnn%i" % (len(s)+1) def nsplit(seq, n=2): """Split a sequence into pieces of length n If the lengt of the sequence isn't a multiple of n, the rest is discareded. Note that nsplit will strings into individual characters. Examples: >>> nsplit('aabbcc') [('a', 'a'), ('b', 'b'), ('c', 'c')] >>> nsplit('aabbcc',n=3) [('a', 'a', 'b'), ('b', 'c', 'c')] # Note that cc is discarded >>> nsplit('aabbcc',n=4) [('a', 'a', 'b', 'b')] """ return [xy for xy in izip(*[iter(seq)]*n)] def chunks(s, cl): """Split a string or sequence into pieces of length cl and return an iterator """ for i in xrange(0, len(s), cl): yield s[i:i+cl] def replace_tags(template, tags, tagsreplace): """Replace occurences of tags with tagreplace Example: >>> replace_tags('a b c d',('b','d'),{'b':'bbb','d':'ddd'}) 'a bbb c ddd' """ s = template for tag in tags: replacestr = tagsreplace.get(tag, '') if not replacestr: replacestr = '' s = s.replace(tag, replacestr) return s def getboolattr(item, key, default): if str(getattr(item,key,'')).lower() == 'true': return True else: return False def create_xdot(dotdata,prog='dot'): # The following code is from the pydot module written by Ero Carrera progs = dotparsing.find_graphviz() #prog = 'dot' if progs is None: log.error('Could not locate Graphviz binaries') return None if not progs.has_key(prog): log.warning('Invalid prog=%s',prog) # Program not found ?!?! return None tmp_fd, tmp_name = tempfile.mkstemp() os.close(tmp_fd) f = open(tmp_name,'w') f.write(dotdata) f.close() format = 'xdot' progpath = '"%s"' % progs[prog].strip() cmd = progpath+' -T'+format+' '+tmp_name log.debug('Creating xdot data with: %s',cmd) stdin, stdout, stderr = os.popen3(cmd,'t') stdin.close() try: data = stdout.read() finally: stdout.close() try: error_data = stderr.read() if error_data: log.debug('Graphviz STDERR %s', error_data) finally: stderr.close() os.unlink(tmp_name) return data def parse_dot_data(dotdata): """Wrapper for pydot.graph_from_dot_data Redirects error messages to the log. """ saveout = sys.stdout fsock = StringIO() sys.stdout = fsock #graph = pydot.graph_from_dot_data(dotdata) parser = dotparsing.DotDataParser() graph = parser.parse_dot_data(dotdata) del(parser) log.debug('Output from dotparser:\n'+fsock.getvalue()) fsock.close() sys.stdout = sys.__stdout__ log.debug('Parsed graph:\n%s',str(graph)) return graph def parse_drawstring(drawstring): """Parse drawstring and returns a list of draw operations""" # The draw string parser is a bit clumsy and slow def doeE(c,s): # E x0 y0 w h Filled ellipse ((x-x0)/w)^2 + ((y-y0)/h)^2 = 1 # e x0 y0 w h Unfilled ellipse ((x-x0)/w)^2 + ((y-y0)/h)^2 = 1 tokens = s.split()[0:4] if not tokens: return None points = map(int,tokens) didx = sum(map(len,tokens))+len(points)+1 return didx, (c , points[0], points[1], points[2], points[3]) def doPLB(c, s): # P n x1 y1 ... xn yn Filled polygon using the given n points # p n x1 y1 ... xn yn Unfilled polygon using the given n points # L n x1 y1 ... xn yn Polyline using the given n points # B n x1 y1 ... xn yn B-spline using the given n control points # b n x1 y1 ... xn yn Filled B-spline using the given n control points tokens = s.split() n = int(tokens[0]) points = map(int,tokens[1:n*2+1]) didx = sum(map(len,tokens[1:n*2+1]))+n*2+2 npoints = nsplit(points, 2) return didx, (c, npoints) def doCS(c,s): # C n -c1c2...cn Set fill color. # c n -c1c2...cn Set pen color. # Graphviz uses the following color formats: # "#%2x%2x%2x" Red-Green-Blue (RGB) # "#%2x%2x%2x%2x" Red-Green-Blue-Alpha (RGBA) # H[, ]+S[, ]+V Hue-Saturation-Value (HSV) 0.0 <= H,S,V <= 1.0 # string color name tokens = s.split() n = int(tokens[0]) tmp = len(tokens[0])+3 d = s[tmp:tmp+n] didx = len(d)+tmp+1 return didx, (c, d) def doFont(c,s): # F s n -c1c2...cn # Set font. The font size is s points. The font name consists of # the n characters following '-'. tokens = s.split() size = tokens[0] n = int(tokens[1]) tmp = len(size)+len(tokens[1])+4 d = s[tmp:tmp+n] didx = len(d)+tmp return didx, (c, size, d) def doText(c,s): # T x y j w n -c1c2...cn # Text drawn using the baseline point (x,y). The text consists of the # n characters following '-'. The text should be left-aligned #(centered, right-aligned) on the point if j is -1 (0, 1), respectively. # The value w gives the width of the text as computed by the library. tokens = s.split() x, y, j, w = tokens[0:4] n = int(tokens[4]) tmp = sum(map(len,tokens[0:5]))+7 text = s[tmp:tmp+n] didx = len(text)+tmp return didx, [c, x, y, j, w, text] cmdlist = [] stat = {} idx = 0 s = drawstring.strip() while idx < len(s)-1: didx = 1 c = s[idx] stat[c] = stat.get(c,0)+1 try: if c in ('e','E'): didx, cmd = doeE(c,s[idx+1:]) cmdlist.append(cmd) elif c in ('p','P','L','b','B'): didx, cmd = doPLB(c, s[idx+1:]) cmdlist.append(cmd) elif c in ('c','C','S'): didx, cmd = doCS(c, s[idx+1:]) cmdlist.append(cmd) elif c == 'F': didx, cmd = doFont(c, s[idx+1:]) cmdlist.append(cmd) elif c == 'T': didx, cmd = doText(c, s[idx+1:]) cmdlist.append(cmd) except: pass idx += didx return cmdlist,stat def get_graphlist(gg, l = []): """Traverse a graph with subgraphs and return them as a list""" if not l: outer = True else: outer = False l.append(gg) if gg.get_subgraphs(): for g in gg.get_subgraphs(): get_graphlist(g,l) if outer: return l class EndOfGraphElement(object): def __init__(self): pass def get_all_graph_elements(graph, l=[]): """Return all nodes and edges, including elements in subgraphs""" if not l: outer = True l.append(graph) else: outer = False for element in graph.allitems: if isinstance(element, dotparsing.DotSubGraph): l.append(element) get_all_graph_elements(element,l) else: l.append(element) if outer: return l else: l.append(EndOfGraphElement()) class DotConvBase(object): """Dot2TeX converter base""" def __init__(self, options = {}): self.color = "" self.opacity = None try: self.template except AttributeError: self.template = options.get('template','') self.textencoding = options.get('encoding',DEFAULT_TEXTENCODING) self.templatevars = {} self.body = "" if options.get('templatefile',''): self.load_template(options['templatefile']) if options.get('template',''): self.template = options['template'] self.options = options if options.get('texpreproc',False) or options.get('autosize',False): self.dopreproc = True else: self.dopreproc = False def load_template(self, templatefile): try: self.template = open(templatefile).read() except: pass def convert_file(self, filename): """Load dot file and convert""" pass def start_fig(self): return "" def end_fig(self): return "" def draw_ellipse(self, drawop, style = None): return "" def draw_bezier(self, drawop, style = None): return "" def draw_polygon(self, drawop, style = None): return "" def draw_polyline(self, drawop, style = None): return "" def draw_text(self, drawop, style = None): return "" def output_node_comment(self, node): return " %% Node: %s\n" % node.name def output_edge_comment(self, edge): src = edge.get_source() dst = edge.get_destination() if self.directedgraph: edge = '->' else: edge = '--' return " %% Edge: %s %s %s\n" % (src, edge, dst) def set_color(self, node): return "" def set_style(self, node): return "" def draw_edge(self, edge): return "" def start_node(self, node): return "" def end_node(self,node): return "" def start_graph(self, graph): return "" def end_graph(self,graph): return "" def start_edge(self): return "" def end_edge(self): return "" def filter_styles(self, style): return style def convert_color(self, drawopcolor,pgf=False): """Convert color to a format usable by LaTeX and XColor""" # Graphviz uses the following color formats: # "#%2x%2x%2x" Red-Green-Blue (RGB) # "#%2x%2x%2x%2x" Red-Green-Blue-Alpha (RGBA) # H[, ]+S[, ]+V Hue-Saturation-Value (HSV) 0.0 <= H,S,V <= 1.0 # string color name # Is the format RBG(A)? if drawopcolor.startswith('#'): t = list(chunks(drawopcolor[1:],2)) # parallell lines not yet supported if len(t) > 6: t = t[0:3] rgb = [(round((int(n,16)/255.0),2)) for n in t] if pgf: colstr = "{rgb}{%s,%s,%s}" % tuple(rgb[0:3]) opacity = "1" if len(rgb)==4: opacity = rgb[3] return (colstr, opacity) else: return "[rgb]{%s,%s,%s}" % tuple(rgb[0:3]) elif (len(drawopcolor.split(' '))==3) or (len(drawopcolor.split(','))==3): # are the values space or comma separated? hsb = drawopcolor.split(',') if not len(hsb) == 3: hsb = drawopcolor.split(' ') if pgf: return "{hsb}{%s,%s,%s}" % tuple(hsb) else: return "[hsb]{%s,%s,%s}" % tuple(hsb) else: drawopcolor = drawopcolor.replace('grey','gray') drawopcolor = drawopcolor.replace('_','') drawopcolor = drawopcolor.replace(' ','') return drawopcolor def do_drawstring(self, drawstring, drawobj): """Parse and draw drawsting Just a wrapper around do_draw_op. """ drawoperations,stat = parse_drawstring(drawstring) return self.do_draw_op(drawoperations, drawobj,stat) def do_draw_op(self, drawoperations, drawobj,stat): """Excecute the operations in drawoperations""" s = "" for drawop in drawoperations: op = drawop[0] style = getattr(drawobj, 'style',None) # styles are not passed to the draw operations in the # duplicate mode if style and not self.options.get('duplicate', False): # map Graphviz styles to backend styles style = self.filter_styles(style) styles = [self.styles.get(key.strip(),key.strip()) \ for key in style.split(',') if key] style = ','.join(styles) else: style = None if op in ['e','E']: s += self.draw_ellipse(drawop, style) elif op in ['p','P']: s += self.draw_polygon(drawop, style) elif op == 'L': s += self.draw_polyline(drawop, style) elif op in ['C','c']: s += self.set_color(drawop) elif op == 'S': s += self.set_style(drawop) elif op in ['B']: s += self.draw_bezier(drawop, style) elif op in ['T']: # Need to decide what to do with the text # Note that graphviz removes the \ character from the draw # string. Use \\ instead # Todo: Use text from node|edge.label or name # Todo: What about multiline labels? text = drawop[5] ## label = getattr(drawobj,'label','\N') ## multiline = False ## if label: ## if label.find(r'\n') >= 0: ## multiline = True ## #print label ## else: ## label = "\N" ## if not multiline and label <> '\N': ## text = drawobj.label texmode = self.options.get('texmode','verbatim') if drawobj.attr.get('texmode', ''): texmode = drawobj.attr['texmode'] if drawobj.attr.get('texlbl', ''): # the texlbl overrides everything text = drawobj.attr['texlbl'] elif texmode == 'verbatim': # verbatim mode text = escape_texchars(text) pass elif texmode == 'math': # math mode text = "$%s$" % text drawop[5] = text if self.options.get('alignstr',''): drawop.append(self.options.get('alignstr')) if stat['T'] == 1 and \ self.options.get('valignmode','center')=='center': # do this for single line only # Todo: Make this optional pos = drawobj.attr.get('lp',None) or \ drawobj.attr.get('pos',None) # force centered alignment drawop[3] = '0' if pos: coord = pos.split(',') if len(coord)==2: drawop[1] = coord[0] drawop[2] = coord[1] pass lblstyle = drawobj.attr.get('lblstyle',None) exstyle = drawobj.attr.get('exstyle','') if exstyle: if lblstyle: lblstyle += ',' +exstyle else: lblstyle = exstyle s += self.draw_text(drawop,lblstyle) return s def do_nodes(self): s = "" for node in self.nodes: self.currentnode = node dstring = node.attr.get('_draw_',"") lstring = node.attr.get('_ldraw_',"") drawstring = dstring+" "+lstring if not drawstring.strip(): continue # detect node type shape = node.attr.get('shape','') if not shape: shape = 'ellipse' # default # extract size information x,y = node.attr.get('pos','').split(',') # width and height are in inches. Convert to bp units w = float(node.attr['width'])*in2bp h = float(node.attr['height'])*in2bp s += self.output_node_comment(node) s += self.start_node(node) #drawoperations = parse_drawstring(drawstring) s += self.do_drawstring(drawstring, node) s += self.end_node(node) self.body += s def get_edge_points(self, edge): # edge BNF # :: (';' )* # :: ? ? + # :: ',' # :: # :: "e" "," "," ## spline ( ';' spline )* ##where spline = (endp)? (startp)? point (triple)+ ##and triple = point point point ##and endp = "e,%d,%d" ##and startp = "s,%d,%d" ##If a spline has points p1 p2 p3 ... pn, (n = 1 (mod 3)), the points correspond to the control points of a B-spline from p1 to pn. If startp is given, it touches one node of the edge, and the arrowhead goes from p1 to startp. If startp is not given, p1 touches a node. Similarly for pn and endp. pos = edge.attr.get('pos') if pos: segments = pos.split(';') else: return [] segret = [] for pos in segments: points = pos.split(' ') # check direction arrowstyle = '--' i = 0 if points[i].startswith('s'): p = points[0].split(',') tmp = "%s,%s" % (p[1],p[2]) if points[1].startswith('e'): points[2] =tmp else: points[1] = tmp del points[0] arrowstyle = '<-' i += 1 if points[0].startswith('e'): p = points[0].split(',') points.pop() points.append("%s,%s" % (p[1],p[2])) del points[0] arrowstyle = '->' i += 1 if i>1: arrowstyle = '<->' segret.append((arrowstyle,points)) return segret def do_edges(self): s = "" s += self.set_color(('cC',"black")) for edge in self.edges: dstring = edge.attr.get('_draw_',"") lstring = edge.attr.get('_ldraw_',"") hstring = edge.attr.get('_hdraw_',"") tstring = edge.attr.get('_tdraw_',"") tlstring = edge.attr.get('_tldraw_',"") hlstring = edge.attr.get('_hldraw_',"") # Note that the order of the draw strings should be the same # as in the xdot output. drawstring = dstring + " " + hstring + " " + tstring \ + " " + lstring + " " + tlstring + " " + hlstring drawop,stat = parse_drawstring(drawstring); if not drawstring.strip(): continue s += self.output_edge_comment(edge) if self.options.get('duplicate', False): s += self.start_edge() s += self.do_draw_op(drawop, edge,stat) s += self.end_edge() else: s += self.draw_edge(edge) s += self.do_drawstring(lstring+" "+tlstring+" "+hlstring, edge) self.body += s def do_graph(self): dstring = self.graph.attr.get('_draw_',"") lstring = self.graph.attr.get('_ldraw_',"") # print lstring # Avoid filling background of graphs with white if dstring.startswith('c 5 -white C 5 -white') \ and not self.graph.attr.get('style'): dstring = '' if getattr(self.graph,'_draw_',None): # bug dstring = "c 5 -black " + dstring #self.graph._draw_ pass drawstring = dstring+" "+lstring if drawstring.strip(): s = self.start_graph(self.graph) g = self.do_drawstring(drawstring, self.graph) e = self.end_graph(self.graph) if g.strip(): self.body += s +g + e def set_options(self): # process options # Warning! If graph attribute is true and command line option is false, # the graph attribute will be used. Command line option should have # precedence. self.options['alignstr'] = self.options.get('alignstr','') \ or getattr(self.maingraph,'d2talignstr','') # Todo: bad! self.options['valignmode'] = getattr(self.maingraph,'d2tvalignmode','')\ or self.options.get('valignmode','center') def convert(self, dotdata): # parse data processed by dot. log.debug('Start conversion') try: try: maingraph = parse_dot_data(dotdata) except: log.info('Failed first attempt to parse graph') if not self.dopreproc: log.info('Could not parse input dotdata directly. ' 'Trying to create xdot data.') try: tmpdata = create_xdot(dotdata,self.options.get('prog','dot')) log.debug('xdotdata:\n'+tmpdata) maingraph = parse_dot_data(tmpdata) log.debug('dotparsing graph:\n'+str(maingraph)) except: raise if not self.dopreproc and not hasattr(maingraph,'xdotversion'): # Older versions of Graphviz does not include the xdotversion # attribute if not (dotdata.find('_draw_') > 0 or dotdata.find('_ldraw_') > 0): # need to convert to xdot format # Warning. Pydot will not include custom attributes log.debug('Trying to create xdotdata') tmpdata = create_xdot(dotdata,self.options.get('prog','dot')) log.debug('xdotdata:\n'+str(tmpdata)) if tmpdata == None or not tmpdata.strip(): log.error('Failed to create xdotdata. Is Graphviz installed?') sys.exit(1) maingraph = parse_dot_data(tmpdata) log.debug('dotparsing graph:\n'+str(maingraph)) else: # old version pass self.maingraph = maingraph self.pencolor = "" self.fillcolor = "" self.linewidth = 1 # Detect graph type self.directedgraph = maingraph.directed except: log.error(helpmsg) raise sys.exit(1) if self.dopreproc: return self.do_preview_preproc() # Romove annoying square # Todo: Remove squares from subgraphs. See pgram.dot dstring = self.maingraph.attr.get('_draw_',"") if dstring: self.maingraph.attr['_draw_'] = "" #setDotAttr(self.maingraph) self.set_options() # A graph can consists of nested graph. Extract all graphs graphlist = get_graphlist(self.maingraph, []) self.body += self.start_fig() # To get correct drawing order we need to iterate over the graphs # multiple times. First we draw the graph graphics, then nodes and # finally the edges. # todo: support the outputorder attribute for graph in graphlist: self.graph = graph self.do_graph() ## if not self.options.get('flattengraph',False): ## for graph in graphlist: ## self.graph = graph ## self.nodes = clean_dot_nodes(graph) ## self.edges = graph.edge_list ## if not self.options.get('switchdraworder',False): ## self.do_edges() # tmp ## self.do_nodes() ## else: ## self.do_nodes() ## self.do_edges() ## else: if True: ## nodelist = [] ## edgelist = [] ## for graph in graphlist: ## self.graph = graph ## nodelist.extend(clean_dot_nodes(graph)) ## edgelist.extend(graph.edge_list) self.nodes = list(maingraph.allnodes) self.edges = list(maingraph.alledges) if not self.options.get('switchdraworder',False): self.do_edges() # tmp self.do_nodes() else: self.do_nodes() self.do_edges() self.body += self.end_fig() return self.output() def clean_template(self, template): """Remove preprocsection or outputsection""" if not self.dopreproc and self.options.get('codeonly',False): r = re.compile('<>(.*?)<>', re.DOTALL | re.MULTILINE) m = r.search(template) if m: return m.group(1).strip() if not self.dopreproc and self.options.get('figonly',False): r = re.compile('<>(.*?)<>', re.DOTALL | re.MULTILINE) m = r.search(template) if m: return m.group(1) r = re.compile('<>(.*?)<>', re.DOTALL | re.MULTILINE) m = r.search(template) if m: return m.group(1) if self.dopreproc: r = re.compile('<>.*?<>', re.DOTALL | re.MULTILINE) else: r = re.compile('<>.*?<>', re.DOTALL | re.MULTILINE) # remove codeonly and figonly section r2 = re.compile('<>.*?<>', re.DOTALL | re.MULTILINE) tmp = r2.sub('',template) r2 = re.compile('<>.*?<>', re.DOTALL | re.MULTILINE) tmp = r2.sub('',tmp) return r.sub('',tmp) def init_template_vars(self): vars = {} # get bounding box bbstr = self.maingraph.attr.get('bb','') if bbstr: bb = bbstr.split(',') vars['<>'] = "(%sbp,%sbp)(%sbp,%sbp)\n" % (bb[0],bb[1],bb[2],bb[3]) vars['<>'] = bb[0] vars['<>'] = bb[1] vars['<>'] = bb[2] vars['<>'] = bb[3] vars['<>'] = self.body.strip() vars['<>'] = self.body.strip() vars['<>'] = self.textencoding docpreamble = self.options.get('docpreamble','') \ or getattr(self.maingraph, 'd2tdocpreamble','') ## if docpreamble: ## docpreamble = docpreamble.replace('\\n','\n') vars['<>'] = docpreamble vars['<>'] = self.options.get('figpreamble','') \ or getattr(self.maingraph, 'd2tfigpreamble','%') vars['<>'] = self.options.get('figpostamble','') \ or getattr(self.maingraph, 'd2tfigpostamble','') vars['<>'] = self.options.get('graphstyle','') \ or getattr(self.maingraph, 'd2tgraphstyle','') vars['<>'] = self.options.get('margin','0pt') vars['<>'] = vars['<>'] = '' vars['<>'] = vars['<>'] = '' if self.options.get('gvcols',False): vars['<>'] = "\input{gvcols.tex}" else: vars['<>'] = "" self.templatevars = vars def output(self): self.init_template_vars() template = self.clean_template(self.template) code = replace_tags(template ,self.templatevars.keys(), self.templatevars) #code = self.template.replace('<>', self.body) return code def get_label(self, drawobj): text = "" texmode = self.options.get('texmode','verbatim') if getattr(drawobj,'texmode', ''): texmode = drawobj.texmode text = getattr(drawobj,'label',None) #log.warning('text %s %s',text,str(drawobj)) if text == None or text.strip() == '\N': if not isinstance(drawobj, dotparsing.DotEdge): text = getattr(drawobj,'name',None) or \ getattr(drawobj,'graph_name','') text = text.replace("\\\\","\\") else: text = '' elif text.strip() == '\N': text = '' else: text = text.replace("\\\\","\\") if getattr(drawobj,'texlbl', ''): # the texlbl overrides everything text = drawobj.texlbl elif texmode == 'verbatim': # verbatim mode text = escape_texchars(text) pass elif texmode == 'math': # math mode text = "$%s$" % text return text # temp def get_labeld(self, drawobj): text = "" texmode = self.options.get('texmode','verbatim') if drawobj.get('texmode', ''): texmode = drawobj["texmode"] text = drawobj.get('label',None) if text == None or text.strip() == '\N': text = drawobj.get('name',None) or \ drawobj.get('graph_name','') else: text = text.replace("\\\\","\\") if drawobj.get('texlbl', ''): # the texlbl overrides everything text = drawobj["texlbl"] elif texmode == 'verbatim': # verbatim mode text = escape_texchars(text) pass elif texmode == 'math': # math mode text = "$%s$" % text return text def get_node_preproc_code(self, node): return node.attr.get('texlbl','') def get_edge_preproc_code(self,edge): return edge.attr.get('texlbl','') def get_graph_preproc_code(self,graph): return graph.attr.get('texlbl','') def get_margins(self, element): """Return element margins""" margins = element.attr.get('margin',None) if margins: margins = margins.split(',') if len(margins) == 1: xmargin = ymargin = float(margins[0]) else: xmargin = float(margins[0]) ymargin = float(margins[1]) else: # use default values if isinstance(element,dotparsing.DotEdge): xmargin = DEFAULT_EDGELABEL_XMARGIN ymargin = DEFAULT_EDGELABEL_YMARGIN else: xmargin = DEFAULT_LABEL_XMARGIN ymargin = DEFAULT_LABEL_YMARGIN return (xmargin, ymargin) # Todo: Add support for head and tail labels! # Todo: Support rect nodes if possible. def do_preview_preproc(self): #setDotAttr(self.maingraph) self.init_template_vars() template = self.clean_template(self.template) template = replace_tags(template ,self.templatevars.keys(), self.templatevars) pp = TeXDimProc(template, self.options) processednodes = {} processededges = {} processedgraphs = {} usednodes = {} usededges = {} usedgraphs = {} # iterate over every element in the graph counter = 0 ## elif isinstance(element, dotparsing.DotSubGraph): ## if not getattr(element,'label',None) and \ ## not getattr(element,'texlbl',None): continue ## name = element.graph_name ## label = self.get_label(element) ## element.texlbl = label ## processedgraphs[name]=element ## ## else: ## pass ## counter += 1 for node in self.maingraph.allnodes: name = node.name if node.attr.get('fixedsize','') == 'true' \ or node.attr.get('style','') in ['invis','invisible']: continue if node.attr.get('shape','') == 'record': log.warning('Record nodes not supported in preprocessing mode: %s',name) continue texlbl = self.get_label(node) if texlbl: node.attr['texlbl'] = texlbl code = self.get_node_preproc_code(node) pp.add_snippet(name,code) usednodes[name] = node for edge in dotparsing.flatten(self.maingraph.alledges): if not edge.attr.get('label'): continue # Ensure that the edge name is unique. name = edge.src.name + edge.dst.name +str(counter) label = self.get_label(edge) edge.attr['texlbl'] = label code = self.get_edge_preproc_code(edge) pp.add_snippet(name,code) usededges[name] = edge counter += 1 for graph in self.maingraph.allgraphs: if not graph.attr.get('label',None) and \ not graph.attr.get('texlbl',None): continue # Make sure that the name is unique name = graph.name + str(counter) counter += 1 label = self.get_label(graph) graph.attr['texlbl'] = label code = self.get_graph_preproc_code(graph) pp.add_snippet(name,code) usedgraphs[name] = graph ok = pp.process() if not ok: errormsg = """\ Failed to preprocess the graph. Is the preview LaTeX package installed? ((Debian package preview-latex-style) To see what happened, run dot2tex with the --debug option. """ log.error(errormsg) sys.exit(1) for name,item in usednodes.items(): if not item.attr.get('texlbl'):continue node = item hp,dp,wt = pp.texdims[name] if self.options.get('rawdim',False): # use dimesions from preview.sty directly node.attr['width'] = wt node.attr['height'] = hp+dp node.attr['label'] = " " node.attr['fixedsize']='true' self.maingraph.allitems.append(node) continue xmargin, ymargin = self.get_margins(node) #print xmargin, ymargin ht = hp+dp minwidth = float(item.attr.get('width') or DEFAULT_NODE_WIDTH) minheight = float(item.attr.get('height') or DEFAULT_NODE_HEIGHT) if self.options.get('nominsize',False): width = wt+2*xmargin height = ht+2*ymargin else: if (wt+2*xmargin) < minwidth: width = minwidth #log.warning("%s %s %s %s %s",minwidth,wt,width,name,tmp) pass else: width = wt+2*xmargin height = ht if ((hp+dp)+2*ymargin) < minheight: height = minheight else: height = ht+ 2*ymargin # Treat shapes with equal widht and height differently # Warning! Rectangles will not alway fit inside a circle # Should use the diagonal. if item.attr.get('shape','') in ['circle','Msquare','doublecircle','Mcircle']: #log.warning('%s %s', name, item['shape']) if wt< height and width < height: width = height else: height=width pass node.attr['width'] = width node.attr['height'] = height node.attr['label'] = " " node.attr['fixedsize']='true' self.maingraph.allitems.append(node) for name,item in usededges.items(): edge = item hp,dp,wt = pp.texdims[name] xmargin, ymargin = self.get_margins(edge) labelcode = '<<'\ ''\ '
a
>>' edge.attr['label']=labelcode % ((wt + 2*xmargin)*72, (hp+dp+2*ymargin)*72) #self.maingraph.allitems.append(edge) for name,item in usedgraphs.items(): graph = item hp,dp,wt = pp.texdims[name] xmargin, ymargin = self.get_margins(graph) labelcode = '<<'\ ''\ '
a
>>' graph.attr['label']=labelcode % ((wt + 2*xmargin)*72, (hp+dp+2*ymargin)*72) self.maingraph.attr['d2toutputformat'] = self.options.get('format', DEFAULT_OUTPUT_FORMAT) graphcode = str(self.maingraph) graphcode = graphcode.replace('<<<','<<') graphcode = graphcode.replace('>>>','>>') return graphcode PSTRICKS_TEMPLATE = r"""\documentclass{article} % <> \usepackage[x11names]{xcolor} \usepackage[<>]{inputenc} \usepackage{graphicx} \usepackage{pstricks} \usepackage{amsmath} <>% \usepackage[active,auctex]{preview} <>% <>% <>% \begin{document} \pagestyle{empty} <>% <>% <>% <>% \enlargethispage{100cm} % Start of code \begin{pspicture}[linewidth=1bp<>]<> \pstVerb{2 setlinejoin} % set line join style to 'mitre' <>% <> <>% \end{pspicture} % End of code <>% \end{document} % <> \begin{pspicture}[linewidth=1bp<>]<> \pstVerb{2 setlinejoin} % set line join style to 'mitre' <>% <> <>% \end{pspicture} <> % <> <>% <> <>% <> """ class Dot2PSTricksConv(DotConvBase): """PSTricks converter backend""" def __init__(self, options = {}): DotConvBase.__init__(self, options) if not self.template: self.template = PSTRICKS_TEMPLATE self.styles = dict( dotted = "linestyle=dotted", dashed = "linestyle=dashed", bold = "linewidth=2pt", solid = "", filled = "", ) def do_graphtmp(self): self.pencolor = ""; self.fillcolor = "" self.color = "" self.body += '{\n' DotConvBase.do_graph(self) self.body += '}\n' def start_fig(self): # get bounding box bbstr = self.maingraph.bb if bbstr: bb = bbstr.split(',') #fillcolor=black, s = "\\begin{pspicture}[linewidth=1bp](%sbp,%sbp)(%sbp,%sbp)\n" % \ (bb[0],bb[1],bb[2],bb[3]) # Set line style to mitre s += " \pstVerb{2 setlinejoin} % set line join style to 'mitre'\n" #return s return "" def end_fig(self): #return '\end{pspicture}\n' return "" def draw_ellipse(self, drawop, style = None): op, x,y,w,h = drawop s = "" #s = " %% Node: %s\n" % node.name if op == 'E': if style: style = style.replace('filled','') stylestr = 'fillstyle=solid' else: stylestr = "" if style: if stylestr: stylestr += ','+ style else: stylestr = style s += " \psellipse[%s](%sbp,%sbp)(%sbp,%sbp)\n" % (stylestr,x,y, \ # w+self.linewidth,h+self.linewidth) w,h) return s def draw_polygon(self, drawop, style = None): op, points = drawop pp = ['(%sbp,%sbp)' % (p[0],p[1]) for p in points] stylestr = "" if op == 'P': if style: style = style.replace('filled','') stylestr = "fillstyle=solid" if style: if stylestr: stylestr += ','+ style else: stylestr = style s = " \pspolygon[%s]%s\n" % (stylestr, "".join(pp)) return s def draw_polyline(self, drawop, style = None): op, points = drawop pp = ['(%sbp,%sbp)' % (p[0],p[1]) for p in points] s = " \psline%s\n" % "".join(pp) return s def draw_bezier(self, drawop, style = None): op, points = drawop pp= [] for point in points: pp.append("(%sbp,%sbp)" % (point[0],point[1])) #points = ['(%sbp, %sbp)' % (p[0],p[1]) for p in points] arrowstyle = "" return " \psbezier{%s}%s\n" % (arrowstyle, "".join(pp)) def draw_text(self, drawop, style=None): if len(drawop)==7: c, x, y, align, w, text, valign = drawop else: c, x, y, align, w, text = drawop valign="" if align == "-1" : alignstr = 'l' # left aligned elif align == "1": alignstr = 'r' # right aligned else: alignstr = "" # centered (default) if alignstr or valign: alignstr = '['+alignstr+valign+']' s = " \\rput%s(%sbp,%sbp){%s}\n" % (alignstr, x,y,text) return s def set_color(self, drawop): c, color = drawop #color = color.replace('grey','gray') #self.pencolor = ""; #self.fillcolor = "" #self.color = "" color = self.convert_color(color) s = "" if c == 'c': # set pen color if self.pencolor <> color: self.pencolor = color s = " \psset{linecolor=%s}\n" % color else: return "" elif c == 'C': # set fill color if self.fillcolor <> color: self.fillcolor = color s = " \psset{fillcolor=%s}\n" % color else: return "" elif c == 'cC': if self.color <> color: self.color = color; self.pencolor = self.fillcolor = color; s = " \psset{linecolor=%s}\n" % (color); else: log.warning('Unhandled color: %s',drawop) return s def set_style(self, drawop): c, style = drawop psstyle = self.styles.get(style,"") if psstyle: return " \psset{%s}\n" % psstyle else: return "" def filter_styles(self, style): fstyles = [] for item in style.split(','): keyval = item.strip() if keyval.find('setlinewidth') < 0: fstyles.append(keyval) return ', '.join(fstyles) def start_node(self, node): self.pencolor = ""; self.fillcolor = "" self.color = "" return "{%\n" def end_node(self,node): return "}%\n" def start_edge(self): self.pencolor = ""; self.fillcolor = "" return "{%\n" def end_edge(self): return "}%\n" def start_graph(self, graph): self.pencolor = ""; self.fillcolor = "" self.color = "" return "{\n" def end_graph(self,node): return "}\n" ## def draw_ellipseNode(self, x,y,w,h,node): ## s = " %% Node: %s\n" % node.name ## s += " \psellipse(%sbp,%sbp)(%sbp,%sbp)\n" % (x,y, \ ## w/2+self.linewidth,h/2+self.linewidth) ## # label ## if node.label: ## label = node.label ## else: ## label = node.name ## s += " \\rput(%sbp,%sbp){$%s$}\n" % (x,y,label) ## ## return s ## def draw_edge(self, edge): s = "" if edge.attr.get('style','') in ['invis','invisible']: return "" edges = self.get_edge_points(edge) for arrowstyle, points in edges: if arrowstyle == '--': arrowstyle='' color = getattr(edge,'color','') if self.color <> color: if color: s += self.set_color(('c',color)) else: # reset to default color s += self.set_color(('c','black')) pp= [] for point in points: p = point.split(',') pp.append("(%sbp,%sbp)" % (p[0],p[1])) edgestyle = edge.attr.get('style','') styles = [] if arrowstyle: styles.append('arrows=%s' % arrowstyle) if edgestyle: edgestyles = [self.styles.get(key.strip(),key.strip()) \ for key in edgestyle.split(',') if key] styles.extend(edgestyles) if styles: stylestr = ",".join(styles) else: stylestr = "" if not self.options.get('straightedges',False): s += " \psbezier[%s]%s\n" % (stylestr,"".join(pp)) else: s += " \psline[%s]%s%s\n" % (stylestr, pp[0], pp[-1]) #s += " \psbezier[%s]{%s}%s\n" % (stylestr, arrowstyle,"".join(pp)) ## if edge.label: ## x,y = edge.lp.split(',') ## #s += "\\rput(%s,%s){%s}\n" % (x,y,edge.label) return s def init_template_vars(self): DotConvBase.init_template_vars(self) # Put a ',' before <> graphstyle = self.templatevars.get('<>','') if graphstyle: graphstyle = graphstyle.strip() if not graphstyle.startswith(','): graphstyle = ','+graphstyle self.templatevars['<>'] = graphstyle PGF_TEMPLATE = r"""\documentclass{article} \usepackage[x11names, rgb]{xcolor} \usepackage[<>]{inputenc} \usepackage{tikz} \usetikzlibrary{snakes,arrows,shapes} \usepackage{amsmath} <>% \usepackage[active,auctex]{preview} <>% <>% <> <>% <> <>% \begin{document} \pagestyle{empty} % <>% <> <>% % <> \enlargethispage{100cm} % Start of code % \begin{tikzpicture}[anchor=mid,>=latex',line join=bevel,<>] \begin{tikzpicture}[>=latex',line join=bevel,<>] \pgfsetlinewidth{1bp} <>% <> <>% \end{tikzpicture} % End of code <> % \end{document} % <> \begin{tikzpicture}[>=latex,line join=bevel,<>] \pgfsetlinewidth{1bp} <>% <> <>% \end{tikzpicture} <> <> <>% <> <>% <> """ PGF118_TEMPLATE = r"""\documentclass{article} \usepackage[x11names, rgb]{xcolor} \usepackage[<>]{inputenc} \usepackage{tikz} \usepackage{pgflibrarysnakes} \usepackage{pgflibraryarrows,pgflibraryshapes} \usepackage{amsmath} <>% \usepackage[active,auctex]{preview} <>% <>% <>% <>% \begin{document} \pagestyle{empty} % <>% <> <>% % <> \enlargethispage{100cm} % Start of code \begin{tikzpicture}[>=latex',join=bevel,<>] \pgfsetlinewidth{1bp} <>% <> <>% \end{tikzpicture} % End of code <> % \end{document} % <> \begin{tikzpicture}[>=latex,join=bevel,<>] \pgfsetlinewidth{1bp} <>% <> <>% \end{tikzpicture} <> <> <>% <> <>% <> """ class Dot2PGFConv(DotConvBase): """PGF/TikZ converter backend""" def __init__(self, options={}): DotConvBase.__init__(self, options) if not self.template: if options.get('pgf118', False): self.template = PGF118_TEMPLATE else: self.template = PGF_TEMPLATE self.styles = dict(dashed='dashed', dotted='dotted', bold='very thick', filled='fill', invis="", rounded='rounded corners', ) self.dashstyles = dict( dashed = '\pgfsetdash{{3pt}{3pt}}{0pt}', dotted='\pgfsetdash{{\pgflinewidth}{2pt}}{0pt}', bold = '\pgfsetlinewidth{1.2pt}') def start_fig(self): # get bounding box # get bounding box s = "" ## bbstr = self.maingraph.bb ## if bbstr: ## bb = bbstr.split(',') ## s += "%%(%sbp,%sbp)(%sbp,%sbp)\n" % \ ## (bb[0],bb[1],bb[2],bb[3]) return s def end_fig(self): #return '\end{tikzpicture}' return "" def start_node(self, node): # Todo: Should find a more elgant solution self.pencolor = ""; self.fillcolor = "" self.color = "" return "\\begin{scope}\n" def end_node(self,node): return "\\end{scope}\n" def start_edge(self): # Todo: Should find a more elgant solution #self.pencolor = ""; #self.fillcolor = "" #self.color = "" return "\\begin{scope}\n" def end_edge(self): return "\\end{scope}\n" def start_graph(self, graph): # Todo: Should find a more elgant solution self.pencolor = ""; self.fillcolor = "" self.color = "" return "\\begin{scope}\n" def end_graph(self,graph): return "\\end{scope}\n" #return "\\end{scope}" def set_color(self, drawop): c, color = drawop # Todo: Should find a more elgant solution #self.pencolor = ""; #self.fillcolor = "" #self.color = "" res = self.convert_color(color, True) opacity = None if len(res)==2: ccolor, opacity = res else: ccolor = res s = "" if c == 'cC': #self.pencolor = color #self.fillcolor = color if self.color <> color: self.color = color self.pencolor=color self.fillcolor=color if ccolor.startswith('{'): # rgb or hsb s += " \definecolor{newcol}%s;\n" % ccolor ccolor = 'newcol' s += " \pgfsetcolor{%s}\n" % ccolor #s += " \pgfsetfillcolor{%s}\n" % ccolor elif c == 'c': # set pen color if self.pencolor <> color: self.pencolor = color self.color = '' if ccolor.startswith('{'): # rgb or hsb s += " \definecolor{strokecol}%s;\n" % ccolor ccolor = 'strokecol' s += " \pgfsetstrokecolor{%s}\n" % ccolor else: return "" elif c == 'C': # set fill color if self.fillcolor <> color: self.fillcolor = color self.color = '' if ccolor.startswith('{'): # rgb s += " \definecolor{fillcol}%s;\n" % ccolor ccolor = 'fillcol' s += " \pgfsetfillcolor{%s}\n" % ccolor if not opacity == None: self.opacity = opacity # Todo: The opacity should probably be set directly when drawing # The \pgfsetfillcopacity cmd affects text as well #s += " \pgfsetfillopacity{%s};\n" % opacity else: self.opacity = None else: return "" return s def set_style(self, drawop): c, style = drawop pgfstyle = self.dashstyles.get(style,"") if pgfstyle: return " %s\n" % pgfstyle else: return "" def filter_styles(self, style): fstyles = [] for item in style.split(','): keyval = item.strip() if keyval.find('setlinewidth') < 0 and not keyval=='filled': fstyles.append(keyval) return ', '.join(fstyles) def draw_ellipse(self, drawop, style = None): op, x,y,w,h = drawop s = "" #s = " %% Node: %s\n" % node.name if op == 'E': if self.opacity <> None: # Todo: Need to know the state of the current node cmd = 'filldraw [opacity=%s]' % self.opacity else: cmd = 'filldraw' else: cmd = "draw" if style: stylestr = " [%s]" % style else: stylestr = '' s += " \%s%s (%sbp,%sbp) ellipse (%sbp and %sbp);\n" % (cmd, stylestr, x,y, \ # w+self.linewidth,h+self.linewidth) w,h) return s def draw_polygon(self, drawop, style = None): op, points = drawop pp = ['(%sbp,%sbp)' % (p[0],p[1]) for p in points] cmd = "draw" if op == 'P': cmd = "filldraw" if style: stylestr = " [%s]" % style else: stylestr = '' s = " \%s%s %s -- cycle;\n" % (cmd, stylestr, " -- ".join(pp)) return s def draw_polyline(self, drawop, style = None): op, points = drawop pp = ['(%sbp,%sbp)' % (p[0],p[1]) for p in points] ##if style: ## stylestr = " [%s]" % style ## else: ## stylestr = '' stylestr = '' return " \draw%s %s;\n" %(stylestr, " -- ".join(pp)) def draw_text(self, drawop, style = None): # The coordinates given by drawop are not the same as the node # coordinates! This may give som odd results if graphviz' and # LaTeX' fonts are very different. if len(drawop)==7: c, x, y, align, w, text, valign = drawop else: c, x, y, align, w, text = drawop valign="" styles = [] if align == "-1" : alignstr = 'right' # left aligned elif align == "1": alignstr = 'left' # right aligned else: alignstr = "" # centered (default) styles.append(alignstr) styles.append(style) ## if alignstr: ## alignstr = "[" + alignstr+", anchor=mid" + "]" ## else: ## alignstr = "[anchor=mid]" lblstyle = ",".join([i for i in styles if i]) if lblstyle: lblstyle = '['+lblstyle+']' s = " \draw (%sbp,%sbp) node%s {%s};\n" % (x,y,lblstyle,text) return s def draw_bezier(self, drawop, style = None): s = "" c, points = drawop arrowstyle = '--' pp= [] for point in points: pp.append("(%sbp,%sbp)" % (point[0],point[1])) #points = ['(%sbp, %sbp)' % (p[0],p[1]) for p in points] #quadp = nsplit(pp, 4) pstrs = ["%s .. controls %s and %s " % p for p in nsplit(pp, 3)] ##if style: ## stylestr = " [%s]" % style ## else: ## stylestr = '' stylestr = '' ## if arrowstyle == '--': ## style = '' ## else: ## style = '[%s]' % arrowstyle s += " \draw%s %s .. %s;\n" % (stylestr, " .. ".join(pstrs), pp[-1]) return s def do_edges(self): s = "" s += self.set_color(('cC',"black")) for edge in self.edges: dstring = getattr(edge,'_draw_',"") lstring = getattr(edge,'_ldraw_',"") hstring = getattr(edge,'_hdraw_',"") tstring = getattr(edge,'_tdraw_',"") tlstring = getattr(edge,'_tldraw_',"") hlstring = getattr(edge,'_hldraw_',"") # Note that the order of the draw strings should be the same # as in the xdot output. drawstring = dstring + " " + hstring + " " + tstring \ + " " + lstring + " " + tlstring + " " + hlstring drawop,stat = parse_drawstring(drawstring); if not drawstring.strip(): continue s += self.output_edge_comment(edge) if self.options.get('duplicate', False): s += self.start_edge() s += self.do_draw_op(drawop, edge,stat) s += self.end_edge() else: topath = getattr(edge,'topath',None) s += self.draw_edge(edge) if not self.options.get('tikzedgelabels',False) and not topath: s += self.do_drawstring(lstring+" "+tlstring+" "+hlstring, edge) else: s += self.do_drawstring(tlstring+" "+hlstring, edge) #s += self.draw_edge(edge) #s += self.do_drawstring(lstring+" "+tlstring+" "+hlstring, edge) self.body += s def draw_edge(self, edge): s = "" if edge.attr.get('style','') in ['invis','invisible']: return "" edges = self.get_edge_points(edge) for arrowstyle, points in edges: #arrowstyle, points = self.get_edge_points(edge) # PGF uses the fill style when drawing some arrowheads. We have to # ensure that the fill color is the same as the pen color. color = getattr(edge,'color','') if self.color <> color: if color: s += self.set_color(('cC',color)) else: # reset to default color s += self.set_color(('cC','black')) pp= [] for point in points: p = point.split(',') pp.append("(%sbp,%sbp)" % (p[0],p[1])) edgestyle = edge.attr.get('style','') styles = [] if arrowstyle <> '--': #styles.append(arrowstyle) styles = [arrowstyle] if edgestyle: edgestyles = [self.styles.get(key.strip(),key.strip()) for key in edgestyle.split(',') if key] styles.extend(edgestyles) stylestr = ",".join(styles) topath = getattr(edge,'topath',None) pstrs = ["%s .. controls %s and %s " % p for p in nsplit(pp, 3)] extra = "" if self.options.get('tikzedgelabels',False) or topath: edgelabel = self.get_label(edge) #log.warning('label: %s', edgelabel) lblstyle = getattr(edge,'lblstyle','') if lblstyle: lblstyle = '['+lblstyle+']' else: lblstyle = '' if edgelabel: extra = " node%s {%s}" % (lblstyle,edgelabel) src = pp[0] dst = pp[-1] if topath: s += " \draw [%s] %s to[%s]%s %s;\n" % (stylestr, src, topath, extra,dst) elif not self.options.get('straightedges',False): #s += " \draw [%s] %s .. %s;\n" % (stylestr, " .. ".join(pstrs), pp[-1]) s += " \draw [%s] %s ..%s %s;\n" % (stylestr, " .. ".join(pstrs), extra,pp[-1]) else: s += " \draw [%s] %s --%s %s;\n" % (stylestr, pp[0],extra, pp[-1]) return s def init_template_vars(self): DotConvBase.init_template_vars(self) if self.options.get('crop',False): cropcode = "\usepackage[active,tightpage]{preview}\n" + \ "\PreviewEnvironment{tikzpicture}\n" + \ "\setlength\PreviewBorder{%s}" % self.options.get('margin','0pt') else: cropcode = "" vars = {} vars['<>'] = cropcode self.templatevars.update(vars) def get_node_preproc_code(self,node): lblstyle = node.attr.get('lblstyle','') text = node.attr.get('texlbl','') if lblstyle: return " \\tikz \\node[%s] {%s};\n" % (lblstyle, text) else: return r"\tikz \node {" + text +"};" def get_edge_preproc_code(self,edge): lblstyle = edge.attr.get('lblstyle','') text = edge.attr.get('texlbl','') if lblstyle: return " \\tikz \\node[%s] {%s};\n" % (lblstyle, text) else: return r"\tikz \node " + "{"+text +"};" def get_graph_preproc_code(self,graph): lblstyle = graph.attr.get('lblstyle','') text = graph.attr.get('texlbl','') if lblstyle: return " \\tikz \\node[%s] {%s};\n" % (lblstyle, text) else: return r"\tikz \node {" + text +"};" TIKZ_TEMPLATE = r"""\documentclass{article} \usepackage[x11names, rgb]{xcolor} \usepackage[<>]{inputenc} \usepackage{tikz} \usetikzlibrary{snakes,arrows,shapes} \usepackage{amsmath} <>% \usepackage[active,auctex]{preview} <>% <>% <> <>% <> <>% \begin{document} \pagestyle{empty} % <>% <> <>% % <> \enlargethispage{100cm} % Start of code \begin{tikzpicture}[>=latex',line join=bevel,<>] <>% <> <>% \end{tikzpicture} % End of code <> % \end{document} % <> \begin{tikzpicture}[>=latex,line join=bevel,<>] <>% <> <>% \end{tikzpicture} <> <> <>% <> <>% <> """ TIKZ118_TEMPLATE = r"""\documentclass{article} \usepackage[x11names, rgb]{xcolor} \usepackage[<>]{inputenc} \usepackage{tikz} \usepackage{pgflibrarysnakes} \usepackage{pgflibraryarrows,pgflibraryshapes} \usepackage{amsmath} <>% \usepackage[active,auctex]{preview} <>% <>% <>% <>% \begin{document} \pagestyle{empty} % <>% <> <>% % <> \enlargethispage{100cm} % Start of code \begin{tikzpicture}[>=latex',join=bevel,<>] <>% <> <>% \end{tikzpicture} % End of code <> % \end{document} % <> \begin{tikzpicture}[>=latex,join=bevel,<>] <>% <> <>% \end{tikzpicture} <> <> <>% <> <>% <> """ class Dot2TikZConv(Dot2PGFConv): """A backend that utilizes the node and edge mechanism of PGF/TikZ""" shapemap = {'doublecircle' : 'circle, double', 'box' : 'rectangle', 'rect' : 'rectangle', 'none' :'draw=none', 'plaintext' : 'draw=none', 'polygon' : 'regular polygon, regular polygon sides=7', 'triangle' : 'regular polygon, regular polygon sides=3', 'pentagon' : 'regular polygon, regular polygon sides=5', 'hexagon' : 'regular polygon, regular polygon sides=5', 'septagon' : 'regular polygon, regular polygon sides=7', 'octagon' : 'regular polygon, regular polygon sides=8', } compassmap = {'n': 'north','ne':'north east','e':'east', 'se':'south east','s':'south','sw':'south west', 'w':'west','nw':'north west','center':'center' } def __init__(self, options={}): # to connect nodes they have to defined. Therefore we have to ensure # that code for generating nodes is outputted first. options['switchdraworder'] = True options['flattengraph'] = True options['rawdim'] = True if options.get('pgf118', False): self.template = TIKZ118_TEMPLATE else: self.template = TIKZ_TEMPLATE DotConvBase.__init__(self, options) self.styles = dict(dashed='dashed', dotted='dotted', bold='very thick', filled='fill', invis="",invisible="", rounded='rounded corners', ) self.dashstyles = dict( dashed = '\pgfsetdash{{3pt}{3pt}}{0pt}', dotted='\pgfsetdash{{\pgflinewidth}{2pt}}{0pt}', bold = '\pgfsetlinewidth{1.2pt}') def set_options(self): Dot2PGFConv.set_options(self) self.options['tikzedgelabels'] = self.options.get('tikzedgelabels','') \ or getboolattr(self.maingraph,'d2ttikzedgelabels','') self.options['styleonly'] = self.options.get('styleonly','') \ or getboolattr(self.maingraph,'d2tstyleonly','') self.options['nodeoptions'] = self.options.get('nodeoptions','') \ or getattr(self.maingraph,'d2tnodeoptions','') self.options['edgeoptions'] = self.options.get('edgeoptions','') \ or getattr(self.maingraph,'d2tedgeoptions','') def output_node_comment(self, node): # With the node syntax comments are unnecessary return "" def set_tikzcolor(self,color,colorname): res = self.convert_color(color, True) if len(res)==2: ccolor, opacity = res if not (opacity == '1'): log.warning('Opacity not supported yet: %s',res) else: ccolor = res s = "" if ccolor.startswith('{'): # rgb or hsb s += " \definecolor{%s}%s;\n" % (colorname,ccolor) cname = colorname else: cname = color return s,cname def get_node_preproc_code(self,node): lblstyle = node.attr.get('lblstyle','') shape = node.attr.get('shape','ellipse') shape = self.shapemap.get(shape, shape) #s += "%% %s\n" % (shape) label = node.attr.get('texlbl','') style = node.attr.get('style'," ") or " "; if lblstyle: if style.strip(): style += ','+lblstyle else: style = lblstyle sn = "" if self.options.get('styleonly'): sn += "\\tikz \\node (%s) [%s] {%s};\n" % \ (style, label) else: sn += "\\tikz \\node [draw,%s,%s] {%s};\n" % \ (shape, style, label) return sn def do_nodes(self): s = "" nodeoptions= self.options.get('nodeoptions',None) if nodeoptions: s += "\\begin{scope}[%s]\n" % nodeoptions for node in self.nodes: self.currentnode = node if node.attr.get('style') in ['invis','invisible']: shape="coordinate" else: # detect node type shape = getattr(node,'shape','ellipse') shape = self.shapemap.get(shape, shape) if shape == None: shape='ellipse' pos = getattr(node,'pos',None) if not pos: continue x,y = pos.split(',') label = self.get_label(node) pos = "%sbp,%sbp" % (x,y) style = node.attr.get('style') or ""; if node.attr.get('lblstyle'): if style: style += ','+node.attr['lblstyle'] else: style = node.attr['lblstyle'] if node.attr.get('exstyle'): if style: style += ','+node.attr['exstyle'] else: style = node.attr['exstyle'] sn = "" sn += self.output_node_comment(node) sn += self.start_node(node) if shape=="coordinate": sn += " \\coordinate (%s) at (%s);\n" % (tikzify(node.name),pos) elif self.options.get('styleonly'): sn += " \\node (%s) at (%s) [%s] {%s};\n" % \ (tikzify(node.name), pos, style, label) else: color = node.attr.get('color','') drawstr = 'draw' if style.strip() == 'filled': fillcolor = node.attr.get('fillcolor') or \ node.attr.get('color') or "gray" drawstr='fill,draw' style = '' if color: code, color = self.set_tikzcolor(color,'strokecolor') sn += code code, fillcolor = self.set_tikzcolor(fillcolor,'fillcolor') sn += code drawstr = "draw=%s,fill=%s" % (color,fillcolor) else: code, fillcolor = self.set_tikzcolor(fillcolor,'fillcolor') sn += code drawstr = "draw,fill=%s" % fillcolor elif color: code, color = self.set_tikzcolor(color,'strokecolor') sn += code drawstr += '='+color if style.strip(): sn += " \\node (%s) at (%s) [%s,%s,%s] {%s};\n" % \ (tikzify(node.name), pos, drawstr, shape, style, label) else: sn += " \\node (%s) at (%s) [%s,%s] {%s};\n" % \ (tikzify(node.name), pos, drawstr, shape, label) sn += self.end_node(node) s += sn if nodeoptions: s += "\\end{scope}\n" self.body += s def do_edges(self): s = "" edgeoptions = self.options.get('edgeoptions',None) if edgeoptions: s += "\\begin{scope}[%s]\n" % edgeoptions for edge in self.edges: dstring = getattr(edge,'_draw_',"") lstring = getattr(edge,'_ldraw_',"") hstring = getattr(edge,'_hdraw_',"") tstring = getattr(edge,'_tdraw_',"") tlstring = getattr(edge,'_tldraw_',"") hlstring = getattr(edge,'_hldraw_',"") topath = getattr(edge,'topath',None) s += self.draw_edge(edge) if not self.options.get('tikzedgelabels',False) and not topath: s += self.do_drawstring(lstring+" "+tlstring+" "+hlstring, edge) else: s += self.do_drawstring(tlstring+" "+hlstring, edge) if edgeoptions: s += "\\end{scope}\n" self.body += s def draw_edge(self, edge): s = "" if edge.attr.get('style','') in ['invis','invisible']: return "" edges = self.get_edge_points(edge) if len(edges) > 1: log.warning('The tikz output format does not support edge'\ 'concentrators yet. Expect ugly output or try the pgf or '\ 'pstricks output formats.') for arrowstyle, points in edges: # PGF uses the fill style when drawing some arrowheads. We have to # ensure that the fill color is the same as the pen color. color = edge.attr.get('color','') pp= [] for point in points: p = point.split(',') pp.append("(%sbp,%sbp)" % (p[0],p[1])) edgestyle = edge.attr.get('style') #print edgestyle styles = [] if arrowstyle <> '--': #styles.append(arrowstyle) styles = [arrowstyle] if edgestyle: edgestyles = [self.styles.get(key.strip(),key.strip()) \ for key in edgestyle.split(',') if key] styles.extend(edgestyles) stylestr = ",".join(styles) if color: code,color = self.set_tikzcolor(color,'strokecolor') s += code; stylestr = color+','+stylestr src = tikzify(edge.get_source()) # check for a port if edge.src_port: src_anchor = self.compassmap.get(edge.src_port.split(':')[-1],'') if src_anchor: src = "%s.%s" % (src,src_anchor) dst = tikzify(edge.get_destination()) if edge.dst_port: dst_anchor = self.compassmap.get(edge.dst_port.split(':')[-1],'') if dst_anchor: dst = "%s.%s" % (dst,dst_anchor) topath = edge.attr.get('topath',None) pstrs = ["%s .. controls %s and %s " % p for p in nsplit(pp, 3)] pstrs[0] = "(%s) ..controls %s and %s " % (src, pp[1], pp[2]) extra = "" if self.options.get('tikzedgelabels',False) or topath: edgelabel = self.get_label(edge) #log.warning('label: %s', edgelabel) lblstyle = getattr(edge,'lblstyle','') exstyle = getattr(edge,'exstyle','') if exstyle: if lblstyle: lblstyle += ',' +exstyle else: lblstyle = exstyle if lblstyle: lblstyle = '['+lblstyle+']' else: lblstyle = '' if edgelabel: extra = " node%s {%s}" % (lblstyle,edgelabel) if topath: s += " \draw [%s] (%s) to[%s]%s (%s);\n" % (stylestr, src, topath, extra,dst) elif not self.options.get('straightedges',False): s += " \draw [%s] %s ..%s (%s);\n" % (stylestr, " .. ".join(pstrs), extra,dst) else: s += " \draw [%s] (%s) --%s (%s);\n" % (stylestr, src, extra,dst) return s def start_node(self, node): return "" def end_node(self,node): return "" PSTRICKSN_TEMPLATE = r"""\documentclass{article} % <> \usepackage[x11names]{xcolor} \usepackage[<>]{inputenc} \usepackage{graphicx} \usepackage{pst-all} \usepackage[a3paper,landscape]{geometry} \usepackage{amsmath} <>% \usepackage[active,auctex]{preview} <>% <>% <>% \begin{document} \pagestyle{empty} <>% <>% <>% <>% \enlargethispage{100cm} % Start of code \begin{pspicture}[linewidth=1bp<>]<> \pstVerb{2 setlinejoin} % set line join style to 'mitre' <>% <> <>% \end{pspicture} % End of code <>% \end{document} % <> \begin{pspicture}[linewidth=1bp<>]<> \pstVerb{2 setlinejoin} % set line join style to 'mitre' <>% <> <>% \end{pspicture} <> % <> <>% <> <>% <> """ # ----------------- mgn -------------- class Dot2PSTricksNConv(Dot2PSTricksConv): """A backend that utilizes the node and edge mechanism of PSTricks-Node""" def __init__(self, options={}): # to connect nodes they have to defined. Therefore we have to ensure # that code for generating nodes is outputted first. options['switchdraworder'] = True options['flattengraph'] = True options['rawdim'] = True self.template = PSTRICKSN_TEMPLATE Dot2PSTricksConv.__init__(self, options) def output_node_comment(self, node): # With the node syntax comments are unnecessary return "" def do_nodes(self): s = "" for node in self.nodes: self.currentnode = node psshadeoption = getattr(node, 'psshadeoption', '') psshape = getattr(node, 'psshape', '') # detect node type, if psshape is not set if len(psshape) == 0: shape = getattr(node, 'shape', 'ellipse') # box -> psframebox # circle -> pscirclebox # rectangle -> psframebox psshape = "psframebox" if shape == "circle": psshape = "pscirclebox" if shape == "ellipse": psshape = "psovalbox" if shape == "triangle": psshape = "pstribox" # TODO incomplete width = getattr(node, 'width', '1') height = getattr(node, 'height', '1') psbox = getattr(node, 'psbox', 'false') color = getattr(node, 'color', '') fillcolor = getattr(node, 'fillcolor', '') if len(color) > 0: psshadeoption = "linecolor=" + color + "," + psshadeoption if len(fillcolor) > 0: psshadeoption = "fillcolor=" + fillcolor + "," + psshadeoption style = getattr(node, 'style', '') if len(style) > 0: if style == "dotted": psshadeoption="linestyle=dotted," + psshadeoption if style == "dashed": psshadeoption="linestyle=dashed," + psshadeoption if style == "solid": psshadeoption="linestyle=solid," + psshadeoption if style == "bold": psshadeoption="linewidth=2pt," + psshadeoption pos = getattr(node, 'pos', None) if not pos: continue x, y = pos.split(',') label = self.get_label(node) pos = "%sbp,%sbp" % (x, y) # TODO style sn = "" sn += self.output_node_comment(node) sn += self.start_node(node) if psbox == "false": sn += "\\rput(%s){\\rnode{%s}{\\%s[%s]{%s}}}\n" % \ (pos, tikzify(node.name), psshape, psshadeoption, label) else: sn += "\\rput(%s){\\rnode{%s}{\\%s[%s]{\parbox[c][%sin][c]{%sin}{\centering %s}}}}\n" % \ (pos, tikzify(node.name), psshape, psshadeoption, height, width, label) sn += self.end_node(node) s += sn self.body += s def do_edges(self): s = "" for edge in self.edges: s += self.draw_edge(edge) self.body += s def draw_edge(self, edge): s = "" edges = self.get_edge_points(edge) for arrowstyle, points in edges: styles = [] psarrow = getattr(edge, 'psarrow', '') if len(psarrow) == 0: stylestr = '-' else: stylestr = psarrow psedge = getattr(edge, 'psedge', 'ncline') psedgeoption = getattr(edge, 'psedgeoption', '') color = getattr(edge, 'color', '') fillcolor = getattr(edge, 'fillcolor', '') if len(color) > 0: psedgeoption = "linecolor=" + color + "," + psedgeoption if len(fillcolor) > 0: psedgeoption = "fillcolor=" + fillcolor + "," + psedgeoption style = getattr(edge, 'style', '') if len(style) > 0: if style == "dotted": psedgeoption="linestyle=dotted," + psedgeoption if style == "dashed": psedgeoption="linestyle=dashed," + psedgeoption if style == "solid": psedgeoption="linestyle=solid," + psedgeoption if style == "bold": psedgeoption="linewidth=2pt," + psedgeoption pslabel = getattr(edge, 'pslabel', 'ncput') pslabeloption = getattr(edge, 'pslabeloption', '') label = getattr(edge, 'label', '') headlabel = getattr(edge, 'headlabel', '') taillabel = getattr(edge, 'taillabel', '') src = tikzify(edge.get_source()) dst = tikzify(edge.get_destination()) s = "\\%s[%s]{%s}{%s}{%s}\n" % (psedge, psedgeoption, stylestr, src, dst) if len(label) != 0: s += "\\%s[%s]{%s}\n" % (pslabel, pslabeloption, label) if len(headlabel) != 0: pslabelhead = 'npos=0.8,' + pslabeloption s += "\\%s[%s]{%s}\n" % (pslabel, pslabelhead, headlabel) if len(taillabel) != 0: pslabeltail = 'npos=0.2,' + pslabeloption s += "\\%s[%s]{%s}\n" % (pslabel, pslabeltail, taillabel) return s def start_node(self, node): return "" def end_node(self, node): return "" # ------------------ mgn ------------------ class PositionsDotConv(Dot2PGFConv): """A converter that returns a dictionary with node positions Returns a dictionary with node name as key and a (x, y) tuple as value. """ def output(self): positions = {} for node in self.nodes: pos = getattr(node, 'pos', None) if pos: positions[node.name] = map(int, pos.split(',')) return positions dimext = r""" ^\!\s Preview:\s Snippet\s (?P\d*)\s ended. \((?P\d*)\+(?P\d*)x(?P\d*)\)""" class TeXDimProc: """Helper class for for finding the size of TeX snippets Uses preview.sty """ # Produce document # Create a temporary directory # Compile file with latex # Parse log file # Update graph with with and height parameters # Clean up def __init__(self,template,options): self.template = template self.snippets_code=[] self.snippets_id=[] self.options = options self.dimext_re = re.compile(dimext,re.MULTILINE|re.VERBOSE) pass def add_snippet(self,id, code): """A a snippet of code to be processed""" self.snippets_id.append(id) self.snippets_code.append(code) def process(self): """Process all snippets of code with TeX and preview.sty Results are stored in the texdimlist and texdims class attributes. Returns False if preprocessing fails """ import shutil if len(self.snippets_code) == 0: log.warning('No labels to preprocess') return True self.tempdir = tempfile.mkdtemp(prefix='dot2tex') log.debug('Creating temporary directroy %s' % self.tempdir) self.tempfilename = os.path.join(self.tempdir,'dot2tex.tex') log.debug('Creating temporary file %s' % self.tempfilename) f = open(self.tempfilename,'w') s = "" for n in self.snippets_code: s += "\\begin{preview}%\n" s += n.strip()+"%\n" s += "\end{preview}%\n" f.write(self.template.replace('<>',s)) #f.flush() f.close() s = open(self.tempfilename,'r').read() log.debug('Code written to %s\n' % self.tempfilename + s) self.parse_log_file() shutil.rmtree(self.tempdir) log.debug('Temporary directory and files deleted') if self.texdims: return True else: return False # cleanup def parse_log_file(self): logfilename = os.path.splitext(self.tempfilename)[0]+'.log' tmpdir = os.getcwd() os.chdir(os.path.split(logfilename)[0]) if self.options.get('usepdflatex',False): command = 'pdflatex -interaction=nonstopmode %s' % self.tempfilename else: command = 'latex -interaction=nonstopmode %s' % self.tempfilename log.debug('Running command: %s' % command) sres = os.popen(command) resdata = sres.read() #log.debug('resdata: %s' % resdata) errcode = sres.close() log.debug('errcode: %s' % errcode) f = open(logfilename,'r') logdata = f.read() log.debug('Logfile from LaTeX run: \n'+logdata) f.close() os.chdir(tmpdir) texdimdata = self.dimext_re.findall(logdata) log.debug('Texdimdata: '+ str(texdimdata)) if len(texdimdata)== 0: log.error('No dimension data could be extracted from dot2tex.tex.') self.texdims = None return c = 1/(100.0*7227) c = 1.0/4736286 self.texdims = {} self.texdimlist = [(float(i[1])*c,float(i[2])*c,float(i[3])*c) for i in texdimdata] #for i in range(len(self.snippets_id)): # self.texdims[self.snippets_id[i]] = self.texdimlist[i] self.texdims = dict(zip(self.snippets_id,self.texdimlist)) def create_options_parser(): """Create and and return an options parser""" usage = "Usage: %prog [options] " parser = OptionParser(usage) parser.add_option("-f", "--format", action="store", dest="format", choices=('pstricks', 'pgf', 'pst', 'tikz', 'psn'), help="Set output format to 'v' (pstrics, pgf) ", metavar="v") parser.add_option('-t','--texmode', dest='texmode', default = 'verbatim', choices = ('math','verbatim', 'raw'), help = "Set text mode (verbatim, math, raw).") parser.add_option('-d', '--duplicate', dest = 'duplicate', action='store_true', default=False, help='Try to duplicate Graphviz graphics') parser.add_option('-s', '--straightedges', dest = 'straightedges', action='store_true', default=False, help='Force straight edges') parser.add_option('--template', dest = 'templatefile', action = 'store', metavar = "FILE") parser.add_option('-o','--output', dest = 'outputfile', action = 'store', metavar = "FILE", default='',help="Write output to FILE") parser.add_option('-e','--encoding', dest = 'encoding', action = 'store', choices = ('utf8','latin1'), default=DEFAULT_TEXTENCODING, help="Set text encoding to utf8 or latin1") parser.add_option('-V','--version', dest = 'printversion', action='store_true', help="Print version information and exit", default=False), parser.add_option('-w','--switchdraworder', dest = 'switchdraworder', action="store_true", help = "Switch draw order", default=False), parser.add_option('-p','-c','--preview', '--crop', dest = 'crop', action = 'store_true', help="Use preview.sty to crop graph", default=False), parser.add_option('--margin', dest = 'margin', action = 'store', help="Set preview margin", default="0pt"), parser.add_option('--docpreamble', dest = 'docpreamble', action = 'store', help="Insert TeX code in document preamble", metavar="TEXCODE"), parser.add_option('--figpreamble', dest = 'figpreamble', action = 'store', help="Insert TeX code in figure preamble", metavar="TEXCODE"), parser.add_option('--figpostamble', dest = 'figpostamble', action = 'store', help="Insert TeX code in figure postamble", metavar="TEXCODE"), parser.add_option('--graphstyle', dest = 'graphstyle', action = 'store', help="Insert graph style", metavar="STYLE"), parser.add_option('--gvcols', dest='gvcols', action ="store_true", default=False, help="Include gvcols.tex"), parser.add_option('--figonly', dest='figonly', action ="store_true", help="Output graph with no preamble", default=False) parser.add_option('--codeonly', dest='codeonly', action ="store_true", help="Output only drawing commands", default=False) parser.add_option('--styleonly', dest='styleonly', action ="store_true", help="Use style parameter only", default=False) parser.add_option('--debug',dest='debug', action="store_true", help="Show additional debugging information", default=False) parser.add_option('--preproc', dest='texpreproc', action="store_true", help = 'Preprocess graph through TeX', default=False) parser.add_option('--alignstr',dest='alignstr',action='store') parser.add_option('--valignmode', dest='valignmode', default = 'center', choices = ('center','dot'), help = "Set vertical alginment mode (center, dot).") parser.add_option('--nominsize', dest='nominsize', action ="store_true", help="No minimum node sizes", default=False) parser.add_option('--usepdflatex', dest='usepdflatex', action ="store_true", help="Use PDFLaTeX for preprocessing", default=False) parser.add_option('--tikzedgelabels', dest='tikzedgelabels', action ="store_true", help="Let TikZ place edge labels", default=False) parser.add_option('--nodeoptions', dest = 'nodeoptions', action = 'store', help="Set options for nodes", metavar="OPTIONS"), parser.add_option('--edgeoptions', dest = 'edgeoptions', action = 'store', help="Set options for edges", metavar="OPTIONS"), parser.add_option('--runtests',dest='runtests', help="Run testes", action="store_true",default=False) parser.add_option("--prog", action="store", dest="prog", default='dot', choices = ('dot','neato','circo','fdp','twopi'), help="Use v to process the graph", metavar="v"), parser.add_option('--autosize',dest='autosize', help="Preprocess graph and then run Graphviz", action="store_true",default=False) parser.add_option('--cache',dest='cache',action='store_true',default=False) parser.add_option('--pgf118', dest='pgf118', action='store_true', help="Generate code compatible with PGF 1.18",default=False) return parser def process_cmd_line(): """Set up and parse command line options""" parser = create_options_parser() (options, args) = parser.parse_args() return options,args,parser def _runtests(): import doctest doctest.testmod() def print_version_info(): print "Dot2tex version % s" % __version__ def load_dot_file(filename): try: dotdata = open(filename,'rU').readlines() except: log.error("Could not open input file %s" % filename) sys.exit(1) log.info('Data read from %s' % filename) return dotdata ## Program interface def main(run_as_module=False,dotdata=None,options=None): """Run dot2tex and convert graph """ import platform global logstream global loghandler if not run_as_module: options, args,parser = process_cmd_line() if options.runtests: log.warning('running tests') _runtests() sys.exit(0) if options.debug: # initalize log handler if run_as_module: if loghandler: #loghandler.flush() log.removeHandler(loghandler) hdlr = logging.StreamHandler(StringIO("")) loghandler = hdlr logstream = hdlr.stream else: hdlr = logging.FileHandler('dot2tex.log') log.addHandler(hdlr) formatter = logging.Formatter('%(asctime)s %(name)s %(levelname)s %(message)s') hdlr.setFormatter(formatter) log.setLevel(logging.DEBUG) nodebug = False else: nodebug = True log.info('------- Start of run -------') log.info("Dot2tex version % s" % __version__) log.info("System information:\n" " Python: %s \n" " Platform: %s\n" " Pyparsing: %s", sys.version_info, platform.platform(), dotparsing.pyparsing_version) log.info('dot2tex called with: %s' % sys.argv) log.info('Program started in %s' % os.getcwd()) if not run_as_module: if options.printversion: #print options.hest print_version_info() sys.exit(0) if len(args) == 0: log.info('Data read from standard input') dotdata = sys.stdin.readlines() elif len(args) == 1: dotdata = load_dot_file(args[0]) else: # Make sure dotdata is compatitle with the readlines data dotdata = dotdata.splitlines(True) s = "" # look for a line containing an \input m = re.search(r"^\s*\\input\{(?P.+?)\}\s*$", "".join(dotdata),re.MULTILINE) if m: filename = m.group(1) log.info('Found \\input{%s}',filename) dotdata = load_dot_file(filename) # I'm not quite sure why this is necessary, but some files # produces data with line endings that confuses pydot/pyparser. # Note: Whitespace at end of line is sometimes significant log.debug('Input data:\n'+"".join(dotdata)) lines = [line for line in dotdata if line.strip()] dotdata = "".join(lines) if options.cache and not run_as_module: import md5, cPickle if len(args) == 1 and options.outputfile: log.info('Caching enabled') inputfilename = args[0] # calculate hash from command line options and dotdata inputhash = md5.new(dotdata + "".join(sys.argv)).hexdigest() log.debug('Hash for %s and command line : %s',inputfilename,inputhash) # now look for a hash file hashfilename = path.join(path.dirname(inputfilename),'dot2tex.cache') key = path.basename(inputfilename) hashes = {} if path.exists(hashfilename): log.info('Loading hash file %s',hashfilename) f = open(hashfilename,'r') try: hashes = cPickle.load(f) except: log.exception('Failed to load hashfile') f.close() if hashes.get(key) == inputhash and path.exists(options.outputfile): log.info('Input has not changed. Will not convert input file') sys.exit(0) else: log.info('Hash or output file not found. Converting file') hashes[key] = inputhash f = open(hashfilename,'w') try: cPickle.dump(hashes, f) except: log.warning('Failed to write hashfile') f.close() else: log.warning('You need to specify an input and output file for caching to work') pass # check for output format attribute fmtattr = re.findall(r'd2toutputformat=([a-z]*)',dotdata) extraoptions = re.findall(r'^\s*d2toptions\s*=\s*"(.*?)"\s*;?',dotdata,re.MULTILINE) if fmtattr: log.info('Found outputformat attribute: %s',fmtattr[0]) gfmt = fmtattr[0] else: gfmt = None if extraoptions: log.debug('Found d2toptions attribute in graph: %s',extraoptions[0]) if run_as_module: parser = create_options_parser() (options, args) = parser.parse_args(extraoptions[0].split(),options) if options.debug and nodebug: # initalize log handler hdlr = logging.FileHandler('dot2tex.log') log.addHandler(hdlr) formatter = logging.Formatter('%(asctime)s %(name)s %(levelname)s %(message)s') hdlr.setFormatter(formatter) log.setLevel(logging.DEBUG) nodebug = False output_format = options.format or gfmt or DEFAULT_OUTPUT_FORMAT options.format = output_format if output_format in ('pstricks', 'pst'): conv = Dot2PSTricksConv(options.__dict__) elif output_format == 'psn': conv = Dot2PSTricksNConv(options.__dict__) elif output_format == 'pgf': conv = Dot2PGFConv(options.__dict__) elif output_format == 'tikz': conv = Dot2TikZConv(options.__dict__) elif output_format == 'positions': conv = PositionsDotConv(options.__dict__) else: log.error("Unknown output format %s" % options.format) sys.exit(1) try: s = conv.convert(dotdata) log.debug('Output:\n%s', s) if options.autosize: conv.dopreproc = False s = conv.convert(s) log.debug('Output:\n%s', s) if options.outputfile: f = open(options.outputfile, 'w') f.write(s) f.close() else: if not run_as_module: print s except SystemExit: if run_as_module: raise except Exception: #log.error("Could not convert the xdot input.") log.exception('Failed to process input') log.info('------- End of run -------') if run_as_module: return s def convert_graph(dotsource,**kwargs): """Process dotsource and return LaTeX code Conversion options can be specified as keyword options. Example: convert_graph(data,format='tikz',crop=True) """ parser = create_options_parser() (options,args) = parser.parse_args([]) if kwargs.get('preproc',None): kwargs['texpreproc'] = kwargs['preproc'] del kwargs['preproc'] options.__dict__.update(kwargs) tex = main(True, dotsource,options) return tex if __name__ == '__main__': main()