#!/usr/bin/python # -*- coding: utf-8 -*- """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-2016, Kjell Magne Fauske """ # Copyright (c) 2006-2016, 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.11.dev' __license__ = 'MIT' import argparse import os.path as path import sys, tempfile, os, re import logging import warnings try: import dotparsing except ImportError: from . import dotparsing # Silence DeprecationWarnings about os.popen3 in Python 2.6 warnings.filterwarnings('ignore', category=DeprecationWarning, message=r'os\.popen3') # Fix from os.popen and os.popen3 to subprocess.Popen try: # https://docs.python.org/3//library/subprocess.html#subprocess-replacements from subprocess import Popen, PIPE def os_popen(command): # pipe = os.popen("cmd", 'r', bufsize) ==> return Popen(command, shell=True, stdout=PIPE).stdout def os_popen3(command, mode): # (child_stdin, child_stdout, child_stderr) = os.popen3("cmd", mode, bufsize) ==> p = Popen(command, shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True) return (p.stdin, p.stdout, p.stderr) except ImportError: os_popen = os.popen os_popen3 = os.popen3 # initialize logging module log = logging.getLogger("dot2tex") 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 INCH2BP = 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_REPLACE = [r'\$', r'$\backslash$', r'\%', r'\_', r'\#', r'\{', r'\}', r'\^{}', r'\&'] charmap = dict(list(zip(SPECIAL_CHARS, SPECIAL_CHARS_REPLACE))) 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 length of the sequence isn't a multiple of n, the rest is discarded. 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 zip(*[iter(seq)] * n)] def chunks(s, cl): """Split a string or sequence into pieces of length cl and return an iterator """ for i in range(0, len(s), cl): yield s[i:i + cl] def replace_tags(template, tags, tagsreplace): """Replace occurrences of tags with tagsreplace 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 smart_float(number): number_as_string = "%s" % float(number) if 'e' in number_as_string: return "%.4f" % float(number) else: return number_as_string def create_xdot(dotdata, prog='dot', options=''): """Run a graph through Graphviz and return an xdot-version of the graph""" # 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 prog in progs: log.error('Invalid prog=%s', prog) raise NameError('The %s program is not recognized. Valid values are %s' % (prog, list(progs.keys()))) tmp_fd, tmp_name = tempfile.mkstemp() os.close(tmp_fd) with open(tmp_name, 'w') as f: f.write(dotdata) output_format = 'xdot' progpath = '"%s"' % progs[prog].strip() cmd = progpath + ' -T' + output_format + ' ' + options + ' ' + 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: if 'Error:' in error_data: log.error("Graphviz returned with the following message: %s", error_data) else: # Graphviz raises a lot of warnings about too small labels, # we therefore log them using log.debug to "hide" them 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. """ parser = dotparsing.DotDataParser() try: graph = parser.parse_dot_data(dotdata) except dotparsing.ParseException: raise finally: del parser 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): """Parse ellipse""" # 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 = list(map(float, tokens)) didx = sum(map(len, tokens)) + len(points) + 1 return didx, (c, points[0], points[1], points[2], points[3]) def doPLB(c, s): """Parse polygon, polyline og B-spline""" # 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 = list(map(float, 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): """Parse fill or pen color""" # 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=None): """Traverse a graph with subgraphs and return them as a list""" if not l: 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=None): """Return all nodes and edges, including elements in subgraphs""" if not l: 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=None): 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 or {} if options.get('texpreproc') or options.get('autosize'): self.dopreproc = True else: self.dopreproc = False def load_template(self, templatefile): try: with open(templatefile) as f: self.template = f.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, texlbl_name="texlbl", use_drawstring_pos=False): """Parse and draw drawsting Just a wrapper around do_draw_op. """ drawoperations, stat = parse_drawstring(drawstring) return self.do_draw_op(drawoperations, drawobj, stat, texlbl_name, use_drawstring_pos) def do_draw_op(self, drawoperations, drawobj, stat, texlbl_name="texlbl", use_drawstring_pos=False): """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'): # 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] # head and tail label texmode = self.options.get('texmode', 'verbatim') if drawobj.attr.get('texmode', ''): texmode = drawobj.attr['texmode'] if texlbl_name in drawobj.attr: # the texlbl overrides everything text = drawobj.attr[texlbl_name] 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 # force centered alignment drawop[3] = '0' if not use_drawstring_pos: if texlbl_name == "tailtexlbl": pos = drawobj.attr.get('tail_lp') or \ drawobj.attr.get('pos') elif texlbl_name == "headtexlbl": pos = drawobj.attr.get('head_lp') or \ drawobj.attr.get('pos') else: pos = drawobj.attr.get('lp') or \ drawobj.attr.get('pos') if pos: coord = pos.split(',') if len(coord) == 2: drawop[1] = coord[0] drawop[2] = coord[1] pass lblstyle = drawobj.attr.get('lblstyle') 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 general_draw_string = node.attr.get('_draw_', "") label_string = node.attr.get('_ldraw_', "") drawstring = general_draw_string + " " + label_string 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']) * INCH2BP # h = float(node.attr['height']) * INCH2BP s += self.output_node_comment(node) s += self.start_node(node) 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 [] return_segments = [] for pos in segments: points = pos.split(' ') # check direction arrow_style = '--' 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] arrow_style = '<-' i += 1 if points[0].startswith('e'): p = points[0].split(',') points.pop() points.append("%s,%s" % (p[1], p[2])) del points[0] arrow_style = '->' i += 1 if i > 1: arrow_style = '<->' arrow_style = self.get_output_arrow_styles(arrow_style, edge) return_segments.append((arrow_style, points)) return return_segments def do_edges(self): s = "" s += self.set_color(('cC', "black")) for edge in self.edges: general_draw_string = edge.attr.get('_draw_', "") label_string = edge.attr.get('_ldraw_', "") head_arrow_string = edge.attr.get('_hdraw_', "") tail_arrow_string = edge.attr.get('_tdraw_', "") tail_label_string = edge.attr.get('_tldraw_', "") head_label_string = edge.attr.get('_hldraw_', "") # Note that the order of the draw strings should be the same # as in the xdot output. drawstring = general_draw_string + " " + head_arrow_string + " " + tail_arrow_string \ + " " + label_string drawop, stat = parse_drawstring(drawstring) if not drawstring.strip(): continue s += self.output_edge_comment(edge) if self.options.get('duplicate'): s += self.start_edge() s += self.do_draw_op(drawop, edge, stat) s += self.do_drawstring(tail_label_string, edge, "tailtexlbl") s += self.do_drawstring(head_label_string, edge, "headtexlbl") s += self.end_edge() else: s += self.draw_edge(edge) s += self.do_drawstring(label_string, edge) s += self.do_drawstring(tail_label_string, edge, "tailtexlbl") s += self.do_drawstring(head_label_string, edge, "headtexlbl") self.body += s def do_graph(self): general_draw_string = self.graph.attr.get('_draw_', "") label_string = self.graph.attr.get('_ldraw_', "") # Avoid filling background of graphs with white if general_draw_string.startswith('c 5 -white C 5 -white') \ and not self.graph.attr.get('style'): general_draw_string = '' if getattr(self.graph, '_draw_', None): # bug general_draw_string = "c 5 -black " + general_draw_string # self.graph._draw_ pass drawstring = general_draw_string + " " + label_string 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.main_graph, 'd2talignstr', '') # Todo: bad! self.options['valignmode'] = getattr(self.main_graph, 'd2tvalignmode', '') \ or self.options.get('valignmode', 'center') def convert(self, dotdata): # parse data processed by dot. log.debug('Start conversion') main_graph = parse_dot_data(dotdata) if not self.dopreproc and not hasattr(main_graph, '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.info('Trying to create xdotdata') tmpdata = create_xdot(dotdata, self.options.get('prog', 'dot'), options=self.options.get('progoptions', '')) if tmpdata is None or not tmpdata.strip(): log.error('Failed to create xdotdata. Is Graphviz installed?') sys.exit(1) log.debug('xdotdata:\n' + str(tmpdata)) main_graph = parse_dot_data(tmpdata) log.debug('dotparsing graph:\n' + str(main_graph)) else: # old version pass self.main_graph = main_graph self.pencolor = "" self.fillcolor = "" self.linewidth = 1 # Detect graph type self.directedgraph = main_graph.directed if self.dopreproc: return self.do_preview_preproc() # Remove annoying square # Todo: Remove squares from subgraphs. See pgram.dot dstring = self.main_graph.attr.get('_draw_', "") if dstring: self.main_graph.attr['_draw_'] = "" self.set_options() # A graph can consists of nested graph. Extract all graphs graphlist = get_graphlist(self.main_graph, []) 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 True: self.nodes = list(main_graph.allnodes) self.edges = list(main_graph.alledges) if not self.options.get('switchdraworder'): 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'): 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'): 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): variables = {} # get bounding box bbstr = self.main_graph.attr.get('bb', '') if bbstr: bb = bbstr.split(',') variables['<>'] = "(%sbp,%sbp)(%sbp,%sbp)\n" % ( smart_float(bb[0]), smart_float(bb[1]), smart_float(bb[2]), smart_float(bb[3])) variables['<>'] = bb[0] variables['<>'] = bb[1] variables['<>'] = bb[2] variables['<>'] = bb[3] variables['<>'] = self.body.strip() variables['<>'] = self.body.strip() variables['<>'] = self.textencoding docpreamble = self.options.get('docpreamble', '') \ or getattr(self.main_graph, 'd2tdocpreamble', '') variables['<>'] = docpreamble variables['<>'] = self.options.get('figpreamble', '') \ or getattr(self.main_graph, 'd2tfigpreamble', '%') variables['<>'] = self.options.get('figpostamble', '') \ or getattr(self.main_graph, 'd2tfigpostamble', '') variables['<>'] = self.options.get('graphstyle', '') \ or getattr(self.main_graph, 'd2tgraphstyle', '') variables['<>'] = self.options.get('margin', '0pt') variables['<>'] = variables['<>'] = '' variables['<>'] = variables['<>'] = '' if self.options.get('gvcols'): variables['<>'] = "\input{gvcols.tex}" else: variables['<>'] = "" self.templatevars = variables def output(self): self.init_template_vars() template = self.clean_template(self.template) code = replace_tags(template, list(self.templatevars.keys()), self.templatevars) return code def get_label(self, drawobj, label_attribute="label", tex_label_attribute="texlbl"): text = "" texmode = self.options.get('texmode', 'verbatim') if getattr(drawobj, 'texmode', ''): texmode = drawobj.texmode text = getattr(drawobj, label_attribute, None) # log.warning('text %s %s',text,str(drawobj)) if text is None or text.strip() == r'\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() == r'\N': text = '' else: text = text.replace("\\\\", "\\") if getattr(drawobj, tex_label_attribute, ''): # 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, attribute="texlbl"): return edge.attr.get(attribute, '') 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') 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, list(self.templatevars.keys()), self.templatevars) pp = TeXDimProc(template, self.options) usednodes = {} usededges = {} usedgraphs = {} # iterate over every element in the graph counter = 0 for node in self.main_graph.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.main_graph.alledges): if not edge.attr.get('label') and not edge.attr.get('texlbl') and not edge.attr.get("headlabel") \ and not edge.attr.get("taillabel"): continue # Ensure that the edge name is unique. name = edge.src.name + edge.dst.name + str(counter) label = self.get_label(edge) headlabel = self.get_label(edge, "headlabel", "headtexlbl") taillabel = self.get_label(edge, "taillabel", "tailtexlbl") if label: name = edge.src.name + edge.dst.name + str(counter) edge.attr['texlbl'] = label code = self.get_edge_preproc_code(edge) pp.add_snippet(name, code) if headlabel: headlabel_name = name + "headlabel" edge.attr['headtexlbl'] = headlabel code = self.get_edge_preproc_code(edge, "headtexlbl") pp.add_snippet(headlabel_name, code) if taillabel: taillabel_name = name + "taillabel" edge.attr['tailtexlbl'] = taillabel code = self.get_edge_preproc_code(edge, "tailtexlbl") pp.add_snippet(taillabel_name, code) counter += 1 usededges[name] = edge for graph in self.main_graph.allgraphs: if not graph.attr.get('label') and not graph.attr.get('texlbl'): 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 list(usednodes.items()): if not item.attr.get('texlbl'): continue node = item hp, dp, wt = pp.texdims[name] if self.options.get('rawdim'): # use dimensions from preview.sty directly node.attr['width'] = wt node.attr['height'] = hp + dp node.attr['label'] = " " node.attr['fixedsize'] = 'true' self.main_graph.allitems.append(node) continue xmargin, ymargin = self.get_margins(node) 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'): width = wt + 2 * xmargin height = ht + 2 * ymargin else: if (wt + 2 * xmargin) < minwidth: width = minwidth else: width = wt + 2 * xmargin height = ht if ((hp + dp) + 2 * ymargin) < minheight: height = minheight else: height = ht + 2 * ymargin # Treat shapes with equal width and height differently # Warning! Rectangles will not always fit inside a circle # Should use the diagonal. if item.attr.get('shape', '') in ['circle', 'Msquare', 'doublecircle', 'Mcircle']: if wt < height and width < height: width = height else: height = width node.attr['width'] = width node.attr['height'] = height node.attr['label'] = " " node.attr['fixedsize'] = 'true' self.main_graph.allitems.append(node) for name, item in list(usededges.items()): edge = item hp, dp, wt = pp.texdims[name] xmargin, ymargin = self.get_margins(edge) labelcode = '<<' \ '' \ '
a
>>' if "texlbl" in edge.attr: edge.attr['label'] = labelcode % ((wt + 2 * xmargin) * 72, (hp + dp + 2 * ymargin) * 72) if "tailtexlbl" in edge.attr: hp, dp, wt = pp.texdims[name + "taillabel"] edge.attr['taillabel'] = labelcode % ((wt + 2 * xmargin) * 72, (hp + dp + 2 * ymargin) * 72) if "headtexlbl" in edge.attr: hp, dp, wt = pp.texdims[name + "headlabel"] edge.attr['headlabel'] = labelcode % ((wt + 2 * xmargin) * 72, (hp + dp + 2 * ymargin) * 72) for name, item in list(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.main_graph.attr['d2toutputformat'] = self.options.get('format', DEFAULT_OUTPUT_FORMAT) graphcode = str(self.main_graph) graphcode = graphcode.replace('<<<', '<<') graphcode = graphcode.replace('>>>', '>>') return graphcode def get_output_arrow_styles(self, arrow_style, edge): return arrow_style PSTRICKS_TEMPLATE = r"""\documentclass{article} % <> \usepackage[x11names,svgnames]{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=None): 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 draw_ellipse(self, drawop, style=None): op, x, y, w, h = drawop s = "" 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, smart_float(x), smart_float(y), # w+self.linewidth,h+self.linewidth) smart_float(w), smart_float(h)) return s def draw_polygon(self, drawop, style=None): op, points = drawop pp = ['(%sbp,%sbp)' % (smart_float(p[0]), smart_float(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)' % (smart_float(p[0]), smart_float(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)" % (smart_float(point[0]), smart_float(point[1]))) 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, smart_float(x), smart_float(y), text) return s def set_color(self, drawop): c, color = drawop 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): filtered_styles = [] for item in style.split(','): keyval = item.strip() if keyval.find('setlinewidth') < 0: filtered_styles.append(keyval) return ', '.join(filtered_styles) 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_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)" % (smart_float(p[0]), smart_float(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'): 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, svgnames, 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} <> <> <>% <> <>% <> """ PGF210_TEMPLATE = r"""\documentclass{article} % dot2tex template for PGF 2.10 \usepackage[x11names, svgnames, 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""" arrows_map_210 = {"dot": "*", "odot": "o", "empty": "open triangle 45", "invempty": "open triangle 45 reversed", "diamond": "diamond", "odiamond": "open diamond", "ediamond": "open diamond", "box": "square", "obox": "open square", "vee": "stealth'", "open": "stealth'", "tee": "|", "crow": "stealth reversed"} def __init__(self, options=None): DotConvBase.__init__(self, options) if not self.template: if options.get('pgf118'): self.template = PGF118_TEMPLATE elif options.get('pgf210'): self.template = PGF210_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_node(self, node): # Todo: Should find a more elegant 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 elegant 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 elegant solution self.pencolor = "" self.fillcolor = "" self.color = "" return "\\begin{scope}\n" def end_graph(self, graph): return "\\end{scope}\n" def set_color(self, drawop): c, color = drawop res = self.convert_color(color, True) opacity = None if len(res) == 2: ccolor, opacity = res else: ccolor = res s = "" if c == 'cC': 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 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 is 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): filtered_styles = [] for item in style.split(','): keyval = item.strip() if keyval.find('setlinewidth') < 0 and not keyval == 'filled': filtered_styles.append(keyval) return ', '.join(filtered_styles) def draw_ellipse(self, drawop, style=None): op, x, y, w, h = drawop s = "" if op == 'E': if self.opacity is not 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, smart_float(x), smart_float(y), # w+self.linewidth,h+self.linewidth) smart_float(w), smart_float(h)) return s def draw_polygon(self, drawop, style=None): op, points = drawop pp = ['(%sbp,%sbp)' % (smart_float(p[0]), smart_float(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)' % (smart_float(p[0]), smart_float(p[1])) for p in points] 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 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) lblstyle = ",".join([i for i in styles if i]) if lblstyle: lblstyle = '[' + lblstyle + ']' s = " \draw (%sbp,%sbp) node%s {%s};\n" % (smart_float(x), smart_float(y), lblstyle, text) return s def draw_bezier(self, drawop, style=None): s = "" c, points = drawop pp = [] for point in points: pp.append("(%sbp,%sbp)" % (smart_float(point[0]), smart_float(point[1]))) pstrs = ["%s .. controls %s and %s " % p for p in nsplit(pp, 3)] stylestr = '' 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: general_draw_string = getattr(edge, '_draw_', "") label_string = getattr(edge, '_ldraw_', "") head_arrow_string = getattr(edge, '_hdraw_', "") tail_arrow_string = getattr(edge, '_tdraw_', "") tail_label_string = getattr(edge, '_tldraw_', "") head_label_string = getattr(edge, '_hldraw_', "") # Note that the order of the draw strings should be the same # as in the xdot output. drawstring = general_draw_string + " " + head_arrow_string + " " + tail_arrow_string \ + " " + label_string draw_operations, stat = parse_drawstring(drawstring) if not drawstring.strip(): continue s += self.output_edge_comment(edge) if self.options.get('duplicate'): s += self.start_edge() s += self.do_draw_op(draw_operations, edge, stat) s += self.do_drawstring(tail_label_string, edge, "tailtexlbl") s += self.do_drawstring(head_label_string, edge, "headtexlbl") s += self.end_edge() else: topath = getattr(edge, 'topath', None) s += self.draw_edge(edge) if not self.options.get('tikzedgelabels') and not topath: s += self.do_drawstring(label_string, edge) s += self.do_drawstring(tail_label_string, edge, "tailtexlbl") s += self.do_drawstring(head_label_string, edge, "headtexlbl") else: s += self.do_drawstring(tail_label_string, edge, "tailtexlbl") s += self.do_drawstring(head_label_string, edge, "headtexlbl") 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)" % (smart_float(p[0]), smart_float(p[1]))) edgestyle = edge.attr.get('style', '') styles = [] if 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 " % x for x in nsplit(pp, 3)] extra = "" if self.options.get('tikzedgelabels') 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'): 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 get_output_arrow_styles(self, arrow_style, edge): dot_arrow_head = edge.attr.get("arrowhead") dot_arrow_tail = edge.attr.get("arrowtail") output_arrow_style = arrow_style if dot_arrow_head: pgf_arrow_head = self.arrows_map_210.get(dot_arrow_head) if pgf_arrow_head: output_arrow_style = output_arrow_style.replace(">", pgf_arrow_head) if dot_arrow_tail: pgf_arrow_tail = self.arrows_map_210.get(dot_arrow_tail) if pgf_arrow_tail: output_arrow_style = output_arrow_style.replace("<", pgf_arrow_tail) return output_arrow_style def init_template_vars(self): DotConvBase.init_template_vars(self) if self.options.get('crop'): cropcode = "\\usepackage[active,tightpage]{preview}\n" + \ "\PreviewEnvironment{tikzpicture}\n" + \ "\setlength\PreviewBorder{%s}" % self.options.get('margin', '0pt') else: cropcode = "" variables = {'<>': cropcode} self.templatevars.update(variables) 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, attribute="texlbl"): lblstyle = edge.attr.get('lblstyle', '') text = edge.attr.get(attribute, '') 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, svgnames, 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} <> <> <>% <> <>% <> """ TIKZ210_TEMPLATE = r"""\documentclass{article} % dot2tex template for PGF 2.10 \usepackage[x11names, svgnames, 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, svgnames, 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""" shape_map = {'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', 'square': 'regular polygon, regular polygon sides=4', 'pentagon': 'regular polygon, regular polygon sides=5', 'hexagon': 'regular polygon, regular polygon sides=6', 'septagon': 'regular polygon, regular polygon sides=7', 'octagon': 'regular polygon, regular polygon sides=8', 'point': 'circle, fill', 'ellipse': 'ellipse', 'oval': 'ellipse', 'diamond': 'diamond', 'trapezium': 'trapezium', 'star': 'star' } compass_map = {'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=None): # to connect nodes they have to defined. Therefore we have to ensure # that code for generating nodes is outputted first. options = options or {} options['switchdraworder'] = True options['flattengraph'] = True options['rawdim'] = True if options.get('pgf118'): self.template = TIKZ118_TEMPLATE elif options.get('pgf210'): self.template = TIKZ210_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.main_graph, 'd2ttikzedgelabels', '') self.options['styleonly'] = self.options.get('styleonly', '') \ or getboolattr(self.main_graph, 'd2tstyleonly', '') self.options['nodeoptions'] = self.options.get('nodeoptions', '') \ or getattr(self.main_graph, 'd2tnodeoptions', '') self.options['edgeoptions'] = self.options.get('edgeoptions', '') \ or getattr(self.main_graph, '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.shape_map.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};\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') if nodeoptions: s += "\\begin{scope}[%s]\n" % nodeoptions for node in self.nodes: self.currentnode = node # detect node type dotshape = getattr(node, 'shape', 'ellipse') shape = None if node.attr.get('style') in ['invis', 'invisible']: shape = "coordinate" else: shape = self.shape_map.get(dotshape, shape) if shape is None: shape = 'ellipse' pos = getattr(node, 'pos', None) if not pos: continue x, y = pos.split(',') if dotshape != 'point': label = self.get_label(node) else: label = '' pos = "%sbp,%sbp" % (smart_float(x), smart_float(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') if edgeoptions: s += "\\begin{scope}[%s]\n" % edgeoptions for edge in self.edges: # general_draw_string = getattr(edge, '_draw_', "") label_string = getattr(edge, '_ldraw_', "") # head_arrow_string = getattr(edge, '_hdraw_', "") # tail_arrow_string = getattr(edge, '_tdraw_', "") tail_label_string = getattr(edge, '_tldraw_', "") head_label_string = getattr(edge, '_hldraw_', "") topath = getattr(edge, 'topath', None) s += self.draw_edge(edge) if not self.options.get('tikzedgelabels') and not topath: s += self.do_drawstring(label_string, edge) s += self.do_drawstring(tail_label_string, edge, "tailtexlbl") s += self.do_drawstring(head_label_string, edge, "headtexlbl") else: s += self.do_drawstring(tail_label_string, edge, "tailtexlbl") s += self.do_drawstring(head_label_string, edge, "headtexlbl") 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)" % (smart_float(p[0]), smart_float(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) 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.compass_map.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.compass_map.get(edge.dst_port.split(':')[-1], '') if dst_anchor: dst = "%s.%s" % (dst, dst_anchor) topath = edge.attr.get('topath') pstrs = ["%s .. controls %s and %s " % x for x in nsplit(pp, 3)] pstrs[0] = "(%s) ..controls %s and %s " % (src, pp[1], pp[2]) extra = "" if self.options.get('tikzedgelabels') 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'): 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, svgnames]{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=None): options = options or {} # 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') if not pos: continue x, y = pos.split(',') label = self.get_label(node) pos = "%sbp,%sbp" % (smart_float(x), smart_float(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: try: positions[node.name] = list(map(int, pos.split(','))) except ValueError: positions[node.name] = list(map(float, pos.split(','))) return positions dimext = r""" ^.*? 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, snippet_id, code): """A a snippet of code to be processed""" self.snippets_id.append(snippet_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 directory %s' % self.tempdir) self.tempfilename = os.path.join(self.tempdir, 'dot2tex.tex') log.debug('Creating temporary file %s' % self.tempfilename) s = "" for n in self.snippets_code: s += "\\begin{preview}%\n" s += n.strip() + "%\n" s += "\end{preview}%\n" with open(self.tempfilename, 'w') as f: f.write(self.template.replace('<>', s)) with open(self.tempfilename, 'r') as f: s = f.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'): 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) errcode = sres.close() log.debug('errcode: %s' % errcode) with open(logfilename, 'r') as f: logdata = f.read() log.debug('Logfile from LaTeX run: \n' + logdata) 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.0 / 4736286 self.texdims = {} self.texdimlist = [(float(i[1]) * c, float(i[2]) * c, float(i[3]) * c) for i in texdimdata] self.texdims = dict(list(zip(self.snippets_id, self.texdimlist))) def create_options_parser(): """Create and and return an options parser. """ description = 'Convert dot files to PGF/TikZ graphics' + \ ' for inclusion in LaTeX.' parser = argparse.ArgumentParser(prog='dot2tex', description=description) parser.add_argument( '-f', '--format', action='store', dest='format', choices=('pstricks', 'pgf', 'pst', 'tikz', 'psn'), help="Set output format to 'v' (pstricks, pgf, pst, tikz, psn) ", metavar="v" ) parser.add_argument( '-t', '--texmode', dest='texmode', default='verbatim', choices=('math', 'verbatim', 'raw'), help='Set text mode (verbatim, math, raw).' ) parser.add_argument( '-d', '--duplicate', dest='duplicate', action='store_true', default=False, help='Try to duplicate Graphviz graphics' ) parser.add_argument( '-s', '--straightedges', dest='straightedges', action='store_true', default=False, help='Force straight edges' ) parser.add_argument( '--template', dest='templatefile', action='store', metavar='FILE' ) parser.add_argument( '-o', '--output', dest='outputfile', action='store', metavar='FILE', default=None, help='Write output to FILE' ) parser.add_argument( '--force', dest='force', action='store_true', default=False, help='Force recompilation, even if output file is newer than input file' ) parser.add_argument( '-e', '--encoding', dest='encoding', action='store', choices=('utf8', 'latin1'), default=DEFAULT_TEXTENCODING, help='Set text encoding to utf8 or latin1' ) parser.add_argument( '-V', '--version', dest='printversion', action='store_true', help='Print version information and exit', default=False ) parser.add_argument( '-w', '--switchdraworder', dest='switchdraworder', action='store_true', help='Switch draw order', default=False ), parser.add_argument( '-p', '-c', '--preview', '--crop', dest='crop', action='store_true', help='Use preview.sty to crop graph', default=False ) parser.add_argument( '--margin', dest='margin', action='store', help='Set preview margin', default='0pt' ) parser.add_argument( '--docpreamble', dest='docpreamble', action='store', help='Insert TeX code in document preamble', metavar='TEXCODE' ) parser.add_argument( '--figpreamble', dest='figpreamble', action='store', help='Insert TeX code in figure preamble', metavar='TEXCODE' ) parser.add_argument( '--figpostamble', dest='figpostamble', action='store', help='Insert TeX code in figure postamble', metavar='TEXCODE' ) parser.add_argument( '--graphstyle', dest='graphstyle', action='store', help='Insert graph style', metavar='STYLE' ) parser.add_argument( '--gvcols', dest='gvcols', action='store_true', default=False, help='Include gvcols.tex' ) parser.add_argument( '--figonly', dest='figonly', action='store_true', help='Output graph with no preamble', default=False ) parser.add_argument( '--codeonly', dest='codeonly', action='store_true', help='Output only drawing commands', default=False ) parser.add_argument( '--styleonly', dest='styleonly', action='store_true', help='Use style parameter only', default=False) parser.add_argument( '--debug', dest='debug', action='store_true', help='Show additional debugging information', default=False ) parser.add_argument( '--preproc', dest='texpreproc', action='store_true', help='Preprocess graph through TeX', default=False ) parser.add_argument('--alignstr', dest='alignstr', action='store') parser.add_argument( '--valignmode', dest='valignmode', default='center', choices=('center', 'dot'), help='Set vertical alginment mode (center, dot).' ) parser.add_argument( '--nominsize', dest='nominsize', action='store_true', help='No minimum node sizes', default=False ) parser.add_argument( '--usepdflatex', dest='usepdflatex', action='store_true', help='Use PDFLaTeX for preprocessing', default=False ) parser.add_argument( '--tikzedgelabels', dest='tikzedgelabels', action='store_true', help='Let TikZ place edge labels', default=False ) parser.add_argument( '--nodeoptions', dest='nodeoptions', action='store', help='Set options for nodes', metavar='OPTIONS' ) parser.add_argument( '--edgeoptions', dest='edgeoptions', action='store', help='Set options for edges', metavar='OPTIONS' ) parser.add_argument( '--runtests', dest='runtests', help="Run testes", action="store_true", default=False ) parser.add_argument( "--prog", action="store", dest="prog", default='dot', choices=('dot', 'neato', 'circo', 'fdp', 'twopi'), help='Use v to process the graph', metavar='v' ) parser.add_argument( '--progoptions', action='store', dest='progoptions', default='', help='Pass options to graph layout engine', metavar='OPTIONS' ) parser.add_argument( '--autosize', dest='autosize', help='Preprocess graph and then run Graphviz', action='store_true', default=False ) parser.add_argument( '--cache', dest='cache', action='store_true', default=False ) parser.add_argument( '--pgf118', dest='pgf118', action='store_true', help='Generate code compatible with PGF 1.18', default=False ) parser.add_argument( '--pgf210', dest='pgf210', action='store_true', help='Generate code compatible with PGF 2.10', default=False ) parser.add_argument( 'inputfile', action='store', nargs='?', default=None, help='Input dot file' ) return parser def process_cmd_line(): """Set up and parse command line options""" parser = create_options_parser() options = parser.parse_args() return options, parser def _runtests(): import doctest doctest.testmod() def print_version_info(): print("Dot2tex version % s" % __version__) def load_dot_file(filename): with open(filename, 'r') as f: dotdata = f.readlines() 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 log if not run_as_module: options, parser = process_cmd_line() # configure console logger 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) if options.runtests: log.warning('running tests') _runtests() sys.exit(0) if options.debug: # initalize log handler if run_as_module: pass 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_version_info() sys.exit(0) if options.inputfile is None: log.info('Data read from standard input') dotdata = sys.stdin.readlines() else: # exit if target file newer than dot source inputfile = options.inputfile outputfile = options.outputfile if outputfile is not None and options.force is False: input_exists = os.access(inputfile, os.F_OK) output_exists = os.access(outputfile, os.F_OK) if input_exists and output_exists: input_modified_time = os.stat(inputfile)[8] output_modified_time = os.stat(outputfile)[8] if input_modified_time < output_modified_time: print('skip: input file older than output file.') sys.exit(0) try: log.debug('Attempting to read data from %s', options.inputfile) dotdata = load_dot_file(options.inputfile) except: if options.debug: log.exception('Failed to load file %s', options.inputfile) else: log.error('Failed to load file %s', options.inputfile) sys.exit(1) 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.+?)\}", "".join(dotdata), re.MULTILINE) if m: filename = m.group(1) log.info('Found \\input{%s}', filename) try: dotdata = load_dot_file(filename) except: if options.debug: log.exception('Failed to load \\input{%s}', filename) else: log.error('Failed to load \\input{%s}', filename) if run_as_module: raise else: sys.exit(1) # 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 hashlib, pickle if options.inputfile is not None and options.outputfile: log.info('Caching enabled') inputfilename = options.inputfile # calculate hash from command line options and dotdata m = hashlib.md5() m.update(dotdata + "".join(sys.argv)) inputhash = m.digest() 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) with open(hashfilename, 'r') as f: try: hashes = pickle.load(f) except: log.exception('Failed to load hashfile') 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 with open(hashfilename, 'w') as f: try: pickle.dump(hashes, f) except: log.warning('Failed to write hashfile') 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 = parser.parse_args(extraoptions[0].split(), options) if options.debug and nodebug: # initalize log handler if not run_as_module: 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 after preprocessing:\n%s', s) if options.outputfile: with open(options.outputfile, 'w') as f: f.write(s) else: if not run_as_module: print(s) except dotparsing.ParseException as err: errmsg = "Parse error:\n%s\n" % err.line + " " * (err.column - 1) + "^\n" + str(err) log.error(errmsg) if options.debug: log.exception('Failed to parse graph') if run_as_module: raise else: log.error(helpmsg) except SystemExit: if run_as_module: raise except: # log.error("Could not convert the xdot input.") log.exception('Failed to process input') if run_as_module: raise 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 = 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()