MoinLight

Annotated moinformat/utils/graphviz.py

101:f92b6c71b4ba
2018-07-29 Paul Boddie Added support for Graphviz regions, storing output using the configured context.
paul@101 1
#!/usr/bin/env python
paul@101 2
paul@101 3
"""
paul@101 4
Graphviz utilities.
paul@101 5
paul@101 6
Copyright (C) 2018 Paul Boddie <paul@boddie.org.uk>
paul@101 7
paul@101 8
This program is free software; you can redistribute it and/or modify it under
paul@101 9
the terms of the GNU General Public License as published by the Free Software
paul@101 10
Foundation; either version 3 of the License, or (at your option) any later
paul@101 11
version.
paul@101 12
paul@101 13
This program is distributed in the hope that it will be useful, but WITHOUT
paul@101 14
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
paul@101 15
FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
paul@101 16
details.
paul@101 17
paul@101 18
You should have received a copy of the GNU General Public License along with
paul@101 19
this program.  If not, see <http://www.gnu.org/licenses/>.
paul@101 20
"""
paul@101 21
paul@101 22
from os.path import exists, join
paul@101 23
from StringIO import StringIO
paul@101 24
from subprocess import Popen, PIPE
paul@101 25
import gzip
paul@101 26
import sha
paul@101 27
import xml.sax
paul@101 28
paul@101 29
# Configurable paths and locations.
paul@101 30
paul@101 31
DIAGRAM_TOOLS_PATH  = "/home/paulb/Software/Graphical/diagram-tools"
paul@101 32
GRAPHVIZ_PATH       = "/usr/bin"
paul@101 33
XSLT_PROCESSOR      = "/usr/bin/xsltproc"
paul@101 34
paul@101 35
# Graphviz "filter" programs performing layout.
paul@101 36
paul@101 37
FILTERS = ['circo', 'dot', 'fdp', 'neato', 'twopi']
paul@101 38
paul@101 39
# Supported output formats.
paul@101 40
paul@101 41
IMAGE_FORMATS = ['png', 'gif']
paul@101 42
SVG_FORMATS = ['svg', 'svgz']
paul@101 43
paul@101 44
OUTPUT_FORMATS = IMAGE_FORMATS + SVG_FORMATS + \
paul@101 45
                 ['dia', 'fig', 'hpgl', 'imap', 'mif', 'pcl', 'ps']
paul@101 46
paul@101 47
# XSL transformations for SVG output.
paul@101 48
paul@101 49
TRANSFORMS = {
paul@101 50
    "notugly" : join(DIAGRAM_TOOLS_PATH, "notugly.xsl"),
paul@101 51
    }
paul@101 52
paul@101 53
paul@101 54
paul@101 55
# Utility functions.
paul@101 56
paul@101 57
def encode(s, encoding):
paul@101 58
paul@101 59
    "Encode 's' using 'encoding' if Unicode."
paul@101 60
paul@101 61
    if isinstance(s, unicode):
paul@101 62
        return s.encode(encoding)
paul@101 63
    else:
paul@101 64
        return s
paul@101 65
paul@101 66
class MetadataParser(xml.sax.handler.ContentHandler):
paul@101 67
paul@101 68
    "Parse metadata from the svg element."
paul@101 69
paul@101 70
    def __init__(self):
paul@101 71
        self.attrs = {}
paul@101 72
paul@101 73
    def startElement(self, name, attrs):
paul@101 74
        if name == self.tagname:
paul@101 75
            self.attrs = dict(attrs)
paul@101 76
paul@101 77
    def parse(self, f):
paul@101 78
paul@101 79
        "Parse content from the file object 'f' using reasonable defaults."
paul@101 80
paul@101 81
        try:
paul@101 82
            parser = xml.sax.make_parser()
paul@101 83
            parser.setContentHandler(self)
paul@101 84
            parser.setErrorHandler(xml.sax.handler.ErrorHandler())
paul@101 85
            parser.setFeature(xml.sax.handler.feature_external_ges, 0)
paul@101 86
            parser.parse(f)
paul@101 87
        finally:
paul@101 88
            f.close()
paul@101 89
paul@101 90
    def get_metadata(self, data, tagname):
paul@101 91
paul@101 92
        "Process 'data', returning attributes from 'tagname'."
paul@101 93
paul@101 94
        self.tagname = tagname
paul@101 95
paul@101 96
        f = StringIO(data)
paul@101 97
        try:
paul@101 98
            self.parse(f)
paul@101 99
        finally:
paul@101 100
            f.close()
paul@101 101
paul@101 102
        return self.attrs
paul@101 103
paul@101 104
def get_output_identifier(text):
paul@101 105
paul@101 106
    "Return an output identifier for the given 'text'."
paul@101 107
paul@101 108
    return sha.new(encode(text, 'utf-8')).hexdigest()
paul@101 109
paul@101 110
def get_program(filter):
paul@101 111
paul@101 112
    "Return the program for the given 'filter'."
paul@101 113
paul@101 114
    if not filter in FILTERS:
paul@101 115
        return None
paul@101 116
    else:
paul@101 117
        return join(GRAPHVIZ_PATH, filter)
paul@101 118
paul@101 119
def transform_output(process, format, transforms):
paul@101 120
paul@101 121
    "Transform the output from 'process' as 'format' using 'transforms'."
paul@101 122
paul@101 123
    # No transformation can occur if the processor is missing.
paul@101 124
paul@101 125
    if not exists(XSLT_PROCESSOR):
paul@101 126
        return process
paul@101 127
paul@101 128
    # Chain transformation processors, each accepting the output of the
paul@101 129
    # preceding one, with the first accepting the initial Graphviz output.
paul@101 130
paul@101 131
    for transform in transforms:
paul@101 132
        stylesheet = TRANSFORMS.get(transform)
paul@101 133
paul@101 134
        # Ignore unrecognised or missing stylesheets.
paul@101 135
paul@101 136
        if not stylesheet or not exists(stylesheet):
paul@101 137
            continue
paul@101 138
paul@101 139
        # Invoke the processor, indicating standard input as the source
paul@101 140
        # document.
paul@101 141
        # Example: /usr/bin/dot /usr/local/share/diagram-tools/notugly.xsl -
paul@101 142
paul@101 143
        process = Popen(
paul@101 144
            [XSLT_PROCESSOR, stylesheet, "-"],
paul@101 145
            shell=False,
paul@101 146
            stdin=process.stdout,
paul@101 147
            stdout=PIPE,
paul@101 148
            stderr=PIPE,
paul@101 149
            close_fds=True)
paul@101 150
paul@101 151
    return process
paul@101 152
paul@101 153
def writefile(s, filename, compressed=False):
paul@101 154
paul@101 155
    "Write 's' to the file having 'filename'."
paul@101 156
paul@101 157
    if compressed:
paul@101 158
        f = gzip.open(filename, "w")
paul@101 159
    else:
paul@101 160
        f = open(filename, "w")
paul@101 161
paul@101 162
    try:
paul@101 163
        f.write(s)
paul@101 164
    finally:
paul@101 165
        f.close()
paul@101 166
paul@101 167
paul@101 168
paul@101 169
# Classes for interacting with Graphviz.
paul@101 170
paul@101 171
class GraphvizError(Exception):
paul@101 172
paul@101 173
    "An error produced when using Graphviz."
paul@101 174
paul@101 175
    def __init__(self, errors):
paul@101 176
        self.errors = errors
paul@101 177
paul@101 178
class Graphviz:
paul@101 179
paul@101 180
    "A Graphviz configuration for single or repeated invocation."
paul@101 181
paul@101 182
    def __init__(self, filter, text, identifier):
paul@101 183
paul@101 184
        """
paul@101 185
        Employ the given 'filter' to produce a graph from the given 'text'. The
paul@101 186
        output 'identifier' for the text is used to provide a filename, if
paul@101 187
        required.
paul@101 188
        """
paul@101 189
paul@101 190
        self.filter = filter
paul@101 191
        self.text = text
paul@101 192
        self.identifier = identifier
paul@101 193
paul@101 194
    def call(self, format, transforms=None, filename=None):
paul@101 195
paul@101 196
        """
paul@101 197
        Invoke Graphviz to produce output in the given 'format'. Any
paul@101 198
        'transforms' are used to transform the output, if appropriate. Any
paul@101 199
        given 'filename' is used to write to a file.
paul@101 200
        """
paul@101 201
paul@101 202
        program = get_program(self.filter)
paul@101 203
paul@101 204
        # Generate uncompressed SVG for later compression.
paul@101 205
paul@101 206
        graphviz_format = format == "svgz" and "svg" or format
paul@101 207
paul@101 208
        # Indicate a filename for direct output for non-SVG formats.
paul@101 209
paul@101 210
        svg = format in SVG_FORMATS
paul@101 211
        options = filename and not svg and ["-o", filename] or []
paul@101 212
paul@101 213
        # Invoke the layout program, with the text to be provided on its
paul@101 214
        # standard input.
paul@101 215
        # Example: /usr/bin/dot -Tsvg -o filename
paul@101 216
paul@101 217
        start = end = Popen(
paul@101 218
            [program, '-T%s' % graphviz_format] + options,
paul@101 219
            shell=False,
paul@101 220
            stdin=PIPE,
paul@101 221
            stdout=PIPE,
paul@101 222
            stderr=PIPE)
paul@101 223
paul@101 224
        # Chain the invocation to transformations, if appropriate.
paul@101 225
paul@101 226
        if svg and transforms:
paul@101 227
            end = transform_output(start, format, transforms)
paul@101 228
paul@101 229
        # Send the graph to the filter.
paul@101 230
paul@101 231
        start.stdin.write(encode(self.text, 'utf-8'))
paul@101 232
paul@101 233
        if end is not start:
paul@101 234
            start.stdin.close()
paul@101 235
paul@101 236
        # Obtain the eventual output.
paul@101 237
paul@101 238
        (self.output, errors) = end.communicate()
paul@101 239
paul@101 240
        # Obtain any metadata.
paul@101 241
paul@101 242
        if svg:
paul@101 243
            parser = MetadataParser()
paul@101 244
            self.metadata = parser.get_metadata(self.output, "svg")
paul@101 245
        elif format == "cmapx":
paul@101 246
            parser = MetadataParser()
paul@101 247
            self.metadata = parser.get_metadata(self.output, "map")
paul@101 248
        else:
paul@101 249
            self.metadata = {}
paul@101 250
paul@101 251
        # Test for errors.
paul@101 252
paul@101 253
        if end.wait() != 0:
paul@101 254
            raise GraphvizError, errors
paul@101 255
paul@101 256
        # Write the file separately, if requested.
paul@101 257
paul@101 258
        if svg and filename:
paul@101 259
            writefile(self.get_output(), filename, format == "svgz")
paul@101 260
paul@101 261
    def get_metadata(self):
paul@101 262
        return self.metadata
paul@101 263
paul@101 264
    def get_output(self):
paul@101 265
        return self.output
paul@101 266
paul@101 267
# vim: tabstop=4 expandtab shiftwidth=4