Index: graphviz/graphviz.py
===================================================================
--- graphviz/graphviz.py	(revision 4543)
+++ graphviz/graphviz.py	(working copy)
@@ -37,7 +37,89 @@
 from trac.wiki.api import IWikiMacroProvider
 from trac.wiki.formatter import extract_link
 
+__all__ = ['Graphviz']
 
+class GraphvizProcessor(object):
+
+    option_categories = []
+    has_svg_support = True
+
+    def __init__(self, processor):
+        self.name = processor
+        self._options = None
+
+    def get_options(self, graphviz):
+        options = []
+        for category in self.option_categories:
+            if category in graphviz.processor_options:
+                options.extend(graphviz.processor_options[category])
+        return options
+
+    def launch(self, graphviz, encoded_input, *args):
+        """Launch a process (cmd), and returns exitcode, stdout + stderr"""
+        # Note: subprocess.Popen doesn't support unicode options arguments
+        # (http://bugs.python.org/issue1759845) so we have to encode them.
+        # Anyway, dot expects utf-8 or the encoding specified with -Gcharset.
+        proc_cmd = graphviz.cmds.get(self.name, self.name)
+        encoded_cmd = [proc_cmd]
+        for arg in list(args) + self.get_options(graphviz):
+            if isinstance(arg, unicode):
+                arg = arg.encode(graphviz.encoding, 'replace')
+            encoded_cmd.append(arg)
+        proc = subprocess.Popen(encoded_cmd, stdin=subprocess.PIPE,
+                                stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+        if encoded_input:
+            proc.stdin.write(encoded_input)
+        proc.stdin.close()
+        out = proc.stdout.read()
+        err = proc.stderr.read()
+        failure = proc.wait() != 0
+        if failure or err or out:
+            return (failure, tag.p(tag.br(), _("The command:"), 
+                         tag.pre(repr(' '.join(encoded_cmd))), 
+                         (_("succeeded but emitted the following output:"),
+                          _("failed with the following output:"))[failure],
+                         out and tag.pre(repr(out)), 
+                         err and tag.pre(repr(err))))
+        else:
+            return (False, None)
+
+
+class DotProcessor(GraphvizProcessor):
+   
+    option_categories = ['graph', 'node', 'edge']
+
+    def process(self, graphviz, encoded_input, format, output_path):
+        return self.launch(graphviz, encoded_input, '-T', format, 
+                           '-o', output_path)
+
+
+
+class MSCGenProcessor(DotProcessor):
+
+    option_categories = []
+    has_svg_support = False
+
+    def process(self, graphviz, encoded_input, format, output_path):
+        f = format
+        if format == 'cmap':
+            f = 'ismap'
+        res = DotProcessor.process(self, graphviz, encoded_input, f, 
+                                   output_path)
+        if format == 'cmap':
+            # convert ismap to cmap
+            ismap = file(output_path, 'r')
+            specs = ismap.readlines()
+            ismap.close()
+            cmap = file(output_path, 'w')
+            for spec in specs:
+                shape, href, coords = spec.strip().split(' ', 2)
+                cmap.write('<area shape="%s" href="%s" coords="%s">' % 
+                           (shape, href, coords))
+            cmap.close()
+        return res
+
+
 class Graphviz(Component):
     """
     Graphviz (http://trac-hacks.org/wiki/GraphvizPlugin) provides
@@ -46,8 +128,12 @@
     """
     implements(IWikiMacroProvider, IHTMLPreviewRenderer, IRequestHandler)
 
-    # Available formats and processors, default first (dot/png)
-    Processors = ['dot', 'neato', 'twopi', 'circo', 'fdp']
+    # Available formats and processors
+    DotProcessors = ['dot', 'neato', 'twopi', 'circo', 'fdp']
+    Processors = dict([(dp, DotProcessor(dp)) for dp in DotProcessors], 
+                      mscgen=MSCGenProcessor('mscgen'),
+                      rsvg=GraphvizProcessor('rsvg'))
+    OptionCategories = {'graph': '-G', 'node': '-N', 'edge': '-E'}
     Bitmap_Formats = ['png', 'jpg', 'gif']
     Vector_Formats = ['svg', 'svgz']
     Formats = Bitmap_Formats + Vector_Formats 
@@ -103,7 +189,7 @@
             basis.
             """)
 
-    processor = Option("graphviz", "processor", Processors[0],
+    default_processor = Option("graphviz", "processor", 'dot',
             """Graphviz default processor. Valid processors
             are: dot, neato, twopi, fdp, circo. If not
             specified, the default is dot. This setting can
@@ -128,6 +214,11 @@
             The default is `<cmd_path>/rsvg`.
             """)
 
+    mscgen_path_option = Option("graphviz", "mscgen_path", "",
+            """Full path to the mscgen program (including the filename).
+            The default is `<cmd_path>/mscgen`.
+            """)
+
     cache_manager = BoolOption("graphviz", "cache_manager", False,
             """If this entry exists and set to true in the configuration file,
             then the cache management logic will be invoked
@@ -168,7 +259,7 @@
 
     def __init__(self):
         self.log.info('version: %s - id: %s' % (__version__, str(__id__)))
-        #self.log.info('processors: %s' % str(Graphviz.Processors))
+        #self.log.info('processors: %s' % repr(Graphviz.Processors))
         #self.log.info('formats: %s' % str(Graphviz.Formats))
 
 
@@ -193,7 +284,6 @@
     def match_request(self, req):
         return req.path_info.startswith('/graphviz')
 
-
     def process_request(self, req):
         # check and load the configuration
         errmsg = self._load_config()
@@ -215,7 +305,7 @@
     def get_macros(self):
         """Return an iterable that provides the names of the provided macros."""
         self._load_config()
-        for p in ['.' + p for p in Graphviz.Processors] + ['']: 
+        for p in ['.' + p for p in Graphviz.Processors.keys()] + ['']: 
             for f in ['/' + f for f in Graphviz.Formats] + ['']:
                 yield 'graphviz%s%s' % (p, f)
 
@@ -259,7 +349,6 @@
 
         content - The text the user entered for the macro to process.
         """
-        req = formatter.req
 
         # check and load the configuration
         errmsg = self._load_config()
@@ -289,16 +378,16 @@
         if not out_format:
             out_format = self.out_format
         if not processor:
-            processor = self.processor
+            processor = self.default_processor
 
-        if processor in Graphviz.Processors:
-            proc_cmd = self.cmds[processor]
-
-        else:
+        if processor not in Graphviz.Processors:
             self.log.error('render_macro: requested processor (%s) not found.' %
                            processor)
             return self._error_div('requested processor (%s) not found.' % 
                                   processor)
+
+        # from now on, use the processor instance
+        processor = Graphviz.Processors[processor]
            
         if out_format not in Graphviz.Formats:
             self.log.error('render_macro: requested format (%s) not found.' %
@@ -308,14 +397,14 @@
                             "requested format (%(fmt)s) not valid.",
                             fmt=out_format)))
 
-        encoded_cmd = (processor + unicode(self.processor_options)) \
-                .encode(self.encoding)
+        encoded_cmd = (processor.name + unicode(processor.get_options(self))).\
+                encode(self.encoding)
         encoded_content = content.encode(self.encoding)
         sha_key  = sha.new(encoded_cmd + encoded_content).hexdigest()
-        img_name = '%s.%s.%s' % (sha_key, processor, out_format)
+        img_name = '%s.%s.%s' % (sha_key, processor.name, out_format)
         # cache: hash.<dot>.<png>
         img_path = os.path.join(self.cache_dir, img_name)
-        map_name = '%s.%s.map' % (sha_key, processor)
+        map_name = '%s.%s.map' % (sha_key, processor.name)
         # cache: hash.<dot>.map
         map_path = os.path.join(self.cache_dir, map_name)
 
@@ -332,25 +421,26 @@
                 encoded_content = content.encode(self.encoding)
 
             # Antialias PNGs with rsvg, if requested
-            if out_format == 'png' and self.png_anti_alias == True:
+            if out_format == 'png' and self.png_anti_alias and \
+                    processor.has_svg_support:
+                svg_path = '%s.svg' % img_path
+
                 # 1. SVG output
-                failure, errmsg = self._launch(
-                        encoded_content, proc_cmd, '-Tsvg', 
-                        '-o%s.svg' % img_path, *self.processor_options)
+                failure, errmsg = processor.process(self, encoded_content, 
+                                                    'svg', svg_path)
                 if failure:
                     return self._error_div(errmsg)
 
                 # 2. SVG to PNG rasterization
-                failure, errmsg = self._launch(
-                        None, self.rsvg_path, '--dpi-x=%d' % self.dpi,
-                        '--dpi-y=%d' % self.dpi, '%s.svg' % img_path, img_path)
+                failure, errmsg = Graphviz.Processors['rsvg'].launch(self,
+                        '--dpi-x=%d' % self.dpi, '--dpi-y=%d' % self.dpi,
+                        svg_path, img_path)
                 if failure:
                     return self._error_div(errmsg)
             
             else: # Render other image formats
-                failure, errmsg = self._launch(
-                        encoded_content, proc_cmd, '-T%s' % out_format,
-                        '-o%s' % img_path, *self.processor_options)
+                failure, errmsg = processor.process(self, encoded_content,
+                                                    out_format, img_path)
                 if failure:
                     return self._error_div(errmsg)
 
@@ -359,9 +449,8 @@
 
                 # Create the map if not in cache
                 if not os.path.exists(map_path):
-                    failure, errmsg = self._launch(
-                            encoded_content, proc_cmd, '-Tcmap',
-                            '-o%s' % map_path, *self.processor_options)
+                    failure, errmsg = processor.process(self, encoded_content,
+                                                        'cmap', map_path)
                     if failure:
                         return self._error_div(errmsg)
 
@@ -477,77 +566,43 @@
                      platform=sys.platform)
 
         self.cmds = {}
-        pname = self._find_cmd(self.processor, cmd_paths)
+        pname = self._find_cmd(self.default_processor, cmd_paths)
         if not pname:
             return _("The default processor '%(proc)s' was not found "
-                     "in '%(paths)s'.", proc=self.processor, paths=cmd_paths)
+                     "in '%(paths)s'.", proc=self.default_processor, 
+                     paths=cmd_paths)
 
-        for name in Graphviz.Processors:
-            pname = self._find_cmd(name, cmd_paths)
+        for name in Graphviz.Processors.keys():
+            pname = (self.config['graphviz'].get('%s_path' % name) or 
+                     self._find_cmd(name, cmd_paths))
 
-            if not pname:
+            if not (pname and os.path.exists(pname)):
                 self.log.warn('The %s program was not found. '
                               'The graphviz/%s macro will be disabled.' %
                               (pname, name))
-                Graphviz.Processors.remove(name)
+                del Graphviz.Processors[name]
 
             self.cmds[name] = pname
 
-        if self.png_anti_alias:
-            self.rsvg_path = (self.rsvg_path_option or 
-                              self._find_cmd('rsvg', cmd_paths))
-            if not (self.rsvg_path and os.path.exists(self.rsvg_path)):
-                return _("The rsvg program is set to '%(path)s' but that path "
-                         "does not exist.", path=self.rsvg_path)
-
         # get default graph/node/edge attributes
-        self.processor_options = []
+        self.processor_options = {}
         defaults = [opt for opt in self.config.options('graphviz') 
                     if opt[0].startswith('default_')]
         for name, value in defaults:
-            for prefix, optkey in [
-                    ('default_graph_', '-G'), 
-                    ('default_node_', '-N'),
-                    ('default_edge_', '-E')]:
+            for category, optkey in Graphviz.OptionCategories.items():
+                prefix = 'default_%s_' % category
                 if name.startswith(prefix):
-                    self.processor_options.append("%s%s=%s" % 
-                            (optkey, name.replace(prefix,''), value))
+                    opts = self.processor_options.setdefault(category, [])
+                    opts.append("%s%s=%s" % (optkey, name.replace(prefix,''), 
+                                             value))
 
         # setup mimetypes to support the IHTMLPreviewRenderer interface
         if 'graphviz' not in MIME_MAP:
             MIME_MAP['graphviz'] = 'application/graphviz'
-        for processor in Graphviz.Processors:
+        for processor in Graphviz.Processors.keys():
             if processor not in MIME_MAP:
                 MIME_MAP[processor] = 'application/graphviz'
 
-    def _launch(self, encoded_input, *args):
-        """Launch a process (cmd), and returns exitcode, stdout + stderr"""
-        # Note: subprocess.Popen doesn't support unicode options arguments
-        # (http://bugs.python.org/issue1759845) so we have to encode them.
-        # Anyway, dot expects utf-8 or the encoding specified with -Gcharset.
-        encoded_cmd = []
-        for arg in args:
-            if isinstance(arg, unicode):
-                arg = arg.encode(self.encoding, 'replace')
-            encoded_cmd.append(arg)
-        p = subprocess.Popen(encoded_cmd, stdin=subprocess.PIPE,
-                             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
-        if encoded_input:
-            p.stdin.write(encoded_input)
-        p.stdin.close()
-        out = p.stdout.read()
-        err = p.stderr.read()
-        failure = p.wait() != 0
-        if failure or err or out:
-            return (failure, tag.p(tag.br(), _("The command:"), 
-                         tag.pre(repr(' '.join(encoded_cmd))), 
-                         (_("succeeded but emitted the following output:"),
-                          _("failed with the following output:"))[failure],
-                         out and tag.pre(repr(out)), 
-                         err and tag.pre(repr(err))))
-        else:
-            return (False, None)
-
     def _error_div(self, msg):
         """Display msg in an error box, using Trac style."""
         if isinstance(msg, str):
