# HG changeset patch # User Paul Boddie # Date 1326555338 -3600 # Node ID a70b2bf1a1a0dedabc617dd14bd470416eb607e8 # Parent f3511ba829c379d64487b71671ec9aa52775b492 Added support for extracting width and height information from SVG images, storing it in attachment filenames and using it when formatting page output. Tidied up the code, removing unnecessary imports, simplifying the argument parsing. diff -r f3511ba829c3 -r a70b2bf1a1a0 graphviz.py --- a/graphviz.py Tue Sep 20 00:02:48 2011 +0200 +++ b/graphviz.py Sat Jan 14 16:35:38 2012 +0100 @@ -4,6 +4,7 @@ Based loosely on GNUPLOT parser by MoinMoin:KwonChanYoung @copyright: 2008 Wayne Tucker + @copyright: 2011, 2012 Paul Boddie @license: GNU GPL, see COPYING for details. """ @@ -64,7 +65,6 @@ quick hack based on looking at an example and digging through the MoinMoin source. The MoinMoin development docs haven't been consulted (yet). - - Only image formats (png and gif) are currently implemented - Comments must start at the beginning of the graphviz block, and at the beginning of their respective lines. They must also not contain any extra whitespace surrounding the = sign. @@ -77,14 +77,9 @@ BINARY_PATH = '/usr/bin' import os -import sys -import base64 -import string -import exceptions -import codecs import subprocess -import time import sha +import re from MoinMoin import config from MoinMoin.action import AttachFile @@ -93,10 +88,9 @@ logging = log.getLogger(__name__) -class GraphVizError(exceptions.RuntimeError): +class GraphVizError(RuntimeError): pass - Dependencies = [] class Parser: @@ -111,12 +105,30 @@ extensions = [] Dependencies = Dependencies + attach_regexp = re.compile( + r"graphviz_" + r"(?P.*?)" + r"(?:" # begin optional section + r"_(?P.*?)_(?P.*?)" # dimensions + r")?" # end optional section + r"\.(?P.*)" + r"$") + + attr_regexp = re.compile( + r"(?Pwidth|height)" + r"\s*=\s*" + r"""(?P['"])""" # start quote + r"(?P.*?)" + r"""(?P=quote)""", # matching quote + re.UNICODE) + def __init__(self, raw, request, **kw): self.raw = raw self.request = request def format(self, formatter): - """ Send the text. """ + + "Using the 'formatter', return the formatted page output." request = self.request page = request.page @@ -125,86 +137,126 @@ request.flush() # to identify error text self.filter = Parser.FILTERS[0] - self.format = 'png' - self.cmapx = None + format = 'png' + cmapx = None + width = None + height = None raw_lines = self.raw.splitlines() for l in raw_lines: if not l[0:2] == '//': break - if l.lower().startswith('//filter='): - tmp = l.split('=', 1)[1].lower() - if tmp in Parser.FILTERS: - self.filter = tmp + + parts = l[2:].split("=") + directive = parts[0] + value = "=".join(parts[1:]) + + if directive == 'filter': + filter = value.lower() + if filter in Parser.FILTERS: + self.filter = filter else: - logging.warn('unknown filter %s' % tmp) - elif l.lower().startswith('//format='): - tmp = l.split('=', 1)[1] - if tmp in Parser.FORMATS: - self.format = tmp - elif l.lower().startswith('//cmapx='): - self.cmapx = wikiutil.escape(l.split('=', 1)[1]) + logging.warn('unknown filter %s' % filter) - if not self.format in Parser.OUTPUT_FORMATS: + elif directive == 'format': + value = value.lower() + if value in Parser.FORMATS: + format = value + + elif directive == 'cmapx': + cmapx = wikiutil.escape(value) + + if not format in Parser.OUTPUT_FORMATS: raise NotImplementedError, "only formats %s are currently supported" % \ Parser.OUTPUT_FORMATS - if self.cmapx: - if not self.format in Parser.IMAGE_FORMATS: - logging.warn('format %s is incompatible with cmapx option' % self.format) - self.cmapx = None + if cmapx: + if not format in Parser.IMAGE_FORMATS: + logging.warn('format %s is incompatible with cmapx option' % format) + cmapx = None - img_name = 'graphviz_%s.%s' % (sha.new(self.raw).hexdigest(), self.format) + digest = sha.new(self.raw).hexdigest() self.pagename = formatter.page.page_name - url = AttachFile.getAttachUrl(self.pagename, img_name, request) - self.attach_dir=AttachFile.getAttachDir(request,self.pagename,create=1) - + self.attach_dir = AttachFile.getAttachDir(request, self.pagename, create=1) self.delete_old_graphs(formatter) - if not os.path.isfile(self.attach_dir + '/' + img_name): - self.graphviz(self.raw, fn='%s/%s' % (self.attach_dir, img_name)) + attrs = self.find_graph(digest, format) + if not attrs: + attrs = self.graphviz(self.raw, digest, format) + + chart = self.get_chartname(digest, format, attrs) + url = AttachFile.getAttachUrl(self.pagename, chart, request) - if self.format in Parser.IMAGE_FORMATS: - if self.cmapx: - request.write('\n' + self.graphviz(self.raw, format='cmapx') + '\n') - request.write(formatter.image(src="%s" % url, usemap="#%s" % self.cmapx)) + if format in Parser.IMAGE_FORMATS: + if cmapx: + request.write('\n' + self.graphviz(self.raw, digest, "cmapx") + '\n') + request.write(formatter.image(src="%s" % url, usemap="#%s" % cmapx, **self.get_format_attrs(attrs))) else: - request.write(formatter.image(src="%s" % url, alt="graphviz image")) + request.write(formatter.image(src="%s" % url, alt="graphviz image", **self.get_format_attrs(attrs))) else: - request.write(formatter.transclusion(1, data=url)) + request.write(formatter.transclusion(1, data=url, **self.get_format_attrs(attrs))) request.write(formatter.text(_("graphviz image"))) request.write(formatter.transclusion(0)) + def find_graph(self, digest, format): + + "Find an existing graph using 'digest' and 'format'." + + attach_files = AttachFile._get_files(self.request, self.pagename) + + for chart in attach_files: + match = self.attach_regexp.match(chart) + + if match and \ + match.group("digest") == digest and \ + match.group("format") == format: + + return match.groupdict() + + return None + + def get_chartname(self, digest, format, attrs): + + "Return the chart name for the 'digest', 'format' and 'attrs'." + + wh = self.get_dimensions(attrs) + if wh: + dimensions = "_%s_%s" % wh + else: + dimensions = "" + return "graphviz_%s%s.%s" % (digest, dimensions, format) + def delete_old_graphs(self, formatter): + + "Using the 'formatter' for page information, delete old graphs." + page_info = formatter.page.lastEditInfo() try: page_date = page_info['time'] - except exceptions.KeyError, ex: + except KeyError, ex: return + attach_files = AttachFile._get_files(self.request, self.pagename) + for chart in attach_files: - if chart.find('graphviz_') == 0 and chart[chart.rfind('.')+1:] in Parser.FORMATS: + match = self.attach_regexp.match(chart) + + if match and match.group("format") in Parser.FORMATS: fullpath = os.path.join(self.attach_dir, chart).encode(config.charset) st = os.stat(fullpath) - chart_date = self.request.user.getFormattedDateTime(st.st_mtime) - if chart_date < page_date : + chart_date = self.request.user.getFormattedDateTime(st.st_mtime) + if chart_date < page_date: os.remove(fullpath) - else : - continue + + def graphviz(self, graph_def, digest, format): - def graphviz(self, graph_def, fn=None, format=None): - if not format: - format = self.format - if fn: - p = subprocess.Popen(['%s/%s' % (BINARY_PATH, self.filter), '-T%s' % format, '-o', fn], shell=False, \ - stdin=subprocess.PIPE, \ - stderr=subprocess.PIPE) - else: - p = subprocess.Popen(['%s/%s' % (BINARY_PATH, self.filter), '-T%s' % format], shell=False, \ - stdin=subprocess.PIPE, \ - stdout=subprocess.PIPE, \ - stderr=subprocess.PIPE) + "Using the 'graph_def' and 'digest', generate output in the given 'format'." + + p = subprocess.Popen(['%s/%s' % (BINARY_PATH, self.filter), '-T%s' % format], shell=False, \ + stdin=subprocess.PIPE, \ + stdout=subprocess.PIPE, \ + stderr=subprocess.PIPE) p.stdin.write(graph_def) p.stdin.flush() @@ -212,17 +264,73 @@ p.wait() - if not fn: - output = p.stdout.read() + # Graph data always goes via standard output so that we can extract the + # width and height if possible. + output, attrs = self.process_output(p.stdout, format) errors = p.stderr.read() + if len(errors) > 0: raise GraphVizError, errors - p = None + # Copy to a file, returning the width and height if possible. + + if format != "cmapx": + chart = self.get_chartname(digest, format, attrs) + filename = os.path.join(self.attach_dir, chart).encode(config.charset) - if fn: - return None + f = open(filename, "wb") + try: + f.write(output) + finally: + f.close() + + return attrs + + # Otherwise, return the output. + else: return output + def process_output(self, output, format): + + "Process graph 'output' in the given 'format'." + + # Return the raw output if SVG is not being produced. + + if format != "svg": + return output.read(), {} + + # Otherwise, return the processed SVG output. + + processed = [] + found = False + attrs = {} + + for line in output.xreadlines(): + if not found and line.startswith("