Merge pull request #472 from prculley/bug9783

This commit is contained in:
Nick Hall
2017-10-15 18:07:04 +01:00
2 changed files with 313 additions and 244 deletions

View File

@@ -8,6 +8,8 @@
# Copyright (C) 2007 Brian G. Matherly # Copyright (C) 2007 Brian G. Matherly
# Copyright (C) 2009 Benny Malengier # Copyright (C) 2009 Benny Malengier
# Copyright (C) 2009 Gary Burton # Copyright (C) 2009 Gary Burton
# Copyright (C) 2017 Mindaugas Baranauskas
# Copyright (C) 2017 Paul Culley
# #
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@@ -23,7 +25,7 @@
# along with this program; if not, write to the Free Software # along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
# #
""" Graphviz adapter for Graphs """
#------------------------------------------------------------------------- #-------------------------------------------------------------------------
# #
# Standard Python modules # Standard Python modules
@@ -34,19 +36,18 @@ import os
from io import BytesIO from io import BytesIO
import tempfile import tempfile
from subprocess import Popen, PIPE from subprocess import Popen, PIPE
import sys
#------------------------------------------------------------------------------- #-------------------------------------------------------------------------
# #
# Gramps modules # Gramps modules
# #
#------------------------------------------------------------------------------- #-------------------------------------------------------------------------
from ...const import GRAMPS_LOCALE as glocale from ...const import GRAMPS_LOCALE as glocale
_ = glocale.translation.gettext _ = glocale.translation.gettext
from ...utils.file import search_for from ...utils.file import search_for, where_is
from . import BaseDoc from . import BaseDoc
from ..menu import NumberOption, TextOption, EnumeratedListOption, \ from ..menu import NumberOption, TextOption, EnumeratedListOption, \
BooleanOption BooleanOption
from ...constfunc import win from ...constfunc import win
#------------------------------------------------------------------------- #-------------------------------------------------------------------------
@@ -55,41 +56,41 @@ from ...constfunc import win
# #
#------------------------------------------------------------------------- #-------------------------------------------------------------------------
import logging import logging
log = logging.getLogger(".graphdoc") LOG = logging.getLogger(".graphdoc")
#------------------------------------------------------------------------------- #-------------------------------------------------------------------------
# #
# Private Constants # Private Constants
# #
#------------------------------------------------------------------------------- #-------------------------------------------------------------------------
_FONTS = [ { 'name' : _("Default"), 'value' : "" }, _FONTS = [{'name' : _("Default"), 'value' : ""},
{ 'name' : _("PostScript / Helvetica"), 'value' : "Helvetica" }, {'name' : _("PostScript / Helvetica"), 'value' : "Helvetica"},
{ 'name' : _("TrueType / FreeSans"), 'value' : "FreeSans" } ] {'name' : _("TrueType / FreeSans"), 'value' : "FreeSans"}]
_RANKDIR = [ { 'name' : _("Vertical (↓)"), 'value' : "TB" }, _RANKDIR = [{'name' : _("Vertical (↓)"), 'value' : "TB"},
{ 'name' : _("Vertical (↑)"), 'value' : "BT" }, {'name' : _("Vertical (↑)"), 'value' : "BT"},
{ 'name' : _("Horizontal (→)"), 'value' : "LR" }, {'name' : _("Horizontal (→)"), 'value' : "LR"},
{ 'name' : _("Horizontal (←)"), 'value' : "RL" } ] {'name' : _("Horizontal (←)"), 'value' : "RL"}]
_PAGEDIR = [ { 'name' : _("Bottom, left"), 'value' :"BL" }, _PAGEDIR = [{'name' : _("Bottom, left"), 'value' : "BL"},
{ 'name' : _("Bottom, right"), 'value' :"BR" }, {'name' : _("Bottom, right"), 'value' : "BR"},
{ 'name' : _("Top, left"), 'value' :"TL" }, {'name' : _("Top, left"), 'value' : "TL"},
{ 'name' : _("Top, Right"), 'value' :"TR" }, {'name' : _("Top, Right"), 'value' : "TR"},
{ 'name' : _("Right, bottom"), 'value' :"RB" }, {'name' : _("Right, bottom"), 'value' : "RB"},
{ 'name' : _("Right, top"), 'value' :"RT" }, {'name' : _("Right, top"), 'value' : "RT"},
{ 'name' : _("Left, bottom"), 'value' :"LB" }, {'name' : _("Left, bottom"), 'value' : "LB"},
{ 'name' : _("Left, top"), 'value' :"LT" } ] {'name' : _("Left, top"), 'value' : "LT"}]
_RATIO = [ { 'name' : _("Compress to minimal size"), 'value': "compress" }, _RATIO = [{'name' : _("Compress to minimal size"), 'value': "compress"},
{ 'name' : _("Fill the given area"), 'value': "fill" }, {'name' : _("Fill the given area"), 'value': "fill"},
{ 'name' : _("Expand uniformly"), 'value': "expand" } ] {'name' : _("Expand uniformly"), 'value': "expand"}]
_NOTELOC = [ { 'name' : _("Top"), 'value' : "t" }, _NOTELOC = [{'name' : _("Top"), 'value' : "t"},
{ 'name' : _("Bottom"), 'value' : "b" }] {'name' : _("Bottom"), 'value' : "b"}]
_SPLINE = [ { 'name' : _("Straight"), 'value' : "false" }, _SPLINE = [{'name' : _("Straight"), 'value' : "false"},
{ 'name' : _("Curved"), 'value' : "true", }, {'name' : _("Curved"), 'value' : "true", },
{ 'name' : _("Orthogonal"), 'value' : 'ortho'} ] {'name' : _("Orthogonal"), 'value' : 'ortho'}]
if win(): if win():
_DOT_FOUND = search_for("dot.exe") _DOT_FOUND = search_for("dot.exe")
@@ -102,26 +103,23 @@ if win():
_GS_CMD = "" _GS_CMD = ""
else: else:
_DOT_FOUND = search_for("dot") _DOT_FOUND = search_for("dot")
_GS_CMD = where_is("gs")
if search_for("gs") == 1:
_GS_CMD = "gs"
else:
_GS_CMD = ""
#------------------------------------------------------------------------------- #------------------------------------------------------------------------------
# #
# GVOptions # GVOptions
# #
#------------------------------------------------------------------------------- #------------------------------------------------------------------------------
class GVOptions: class GVOptions:
""" """
Defines all of the controls necessary Defines all of the controls necessary
to configure the graph reports. to configure the graph reports.
""" """
def __init__(self): def __init__(self):
self.h_pages = None self.h_pages = None
self.v_pages = None self.v_pages = None
self.page_dir = None self.page_dir = None
self.dpi = None self.dpi = None
def add_menu_options(self, menu): def add_menu_options(self, menu):
@@ -139,9 +137,9 @@ class GVOptions:
for item in _FONTS: for item in _FONTS:
font_family.add_item(item["value"], item["name"]) font_family.add_item(item["value"], item["name"])
font_family.set_help(_("Choose the font family. If international " font_family.set_help(_("Choose the font family. If international "
"characters don't show, use FreeSans font. " "characters don't show, use FreeSans font. "
"FreeSans is available from: " "FreeSans is available from: "
"http://www.nongnu.org/freefont/")) "http://www.nongnu.org/freefont/"))
menu.add_option(category, "font_family", font_family) menu.add_option(category, "font_family", font_family)
font_size = NumberOption(_("Font size"), 14, 8, 128) font_size = NumberOption(_("Font size"), 14, 8, 128)
@@ -188,9 +186,9 @@ class GVOptions:
# the page direction option only makes sense when the # the page direction option only makes sense when the
# number of horizontal and/or vertical pages is > 1, # number of horizontal and/or vertical pages is > 1,
# so we need to remember these 3 controls for later # so we need to remember these 3 controls for later
self.h_pages = h_pages self.h_pages = h_pages
self.v_pages = v_pages self.v_pages = v_pages
self.page_dir = page_dir self.page_dir = page_dir
# the page direction option only makes sense when the # the page direction option only makes sense when the
# number of horizontal and/or vertical pages is > 1 # number of horizontal and/or vertical pages is > 1
@@ -204,7 +202,8 @@ class GVOptions:
aspect_ratio = EnumeratedListOption(_("Aspect ratio"), "fill") aspect_ratio = EnumeratedListOption(_("Aspect ratio"), "fill")
for item in _RATIO: for item in _RATIO:
aspect_ratio.add_item(item["value"], item["name"]) aspect_ratio.add_item(item["value"], item["name"])
help_text = _('Affects node spacing and scaling of the graph.\n' help_text = _(
'Affects node spacing and scaling of the graph.\n'
'If the graph is smaller than the print area:\n' 'If the graph is smaller than the print area:\n'
' Compress will not change the node spacing. \n' ' Compress will not change the node spacing. \n'
' Fill will increase the node spacing to fit the print area in ' ' Fill will increase the node spacing to fit the print area in '
@@ -221,34 +220,34 @@ class GVOptions:
menu.add_option(category, "ratio", aspect_ratio) menu.add_option(category, "ratio", aspect_ratio)
dpi = NumberOption(_("DPI"), 75, 20, 1200) dpi = NumberOption(_("DPI"), 75, 20, 1200)
dpi.set_help(_( "Dots per inch. When creating images such as " dpi.set_help(_("Dots per inch. When creating images such as "
".gif or .png files for the web, try numbers " ".gif or .png files for the web, try numbers "
"such as 100 or 300 DPI. PostScript and PDF files " "such as 100 or 300 DPI. PostScript and PDF files "
"always use 72 DPI.")) "always use 72 DPI."))
menu.add_option(category, "dpi", dpi) menu.add_option(category, "dpi", dpi)
self.dpi = dpi self.dpi = dpi
nodesep = NumberOption(_("Node spacing"), 0.20, 0.01, 5.00, 0.01) nodesep = NumberOption(_("Node spacing"), 0.20, 0.01, 5.00, 0.01)
nodesep.set_help(_( "The minimum amount of free space, in inches, " nodesep.set_help(_("The minimum amount of free space, in inches, "
"between individual nodes. For vertical graphs, " "between individual nodes. For vertical graphs, "
"this corresponds to spacing between columns. " "this corresponds to spacing between columns. "
"For horizontal graphs, this corresponds to " "For horizontal graphs, this corresponds to "
"spacing between rows.")) "spacing between rows."))
menu.add_option(category, "nodesep", nodesep) menu.add_option(category, "nodesep", nodesep)
ranksep = NumberOption(_("Rank spacing"), 0.20, 0.01, 5.00, 0.01) ranksep = NumberOption(_("Rank spacing"), 0.20, 0.01, 5.00, 0.01)
ranksep.set_help(_( "The minimum amount of free space, in inches, " ranksep.set_help(_("The minimum amount of free space, in inches, "
"between ranks. For vertical graphs, this " "between ranks. For vertical graphs, this "
"corresponds to spacing between rows. For " "corresponds to spacing between rows. For "
"horizontal graphs, this corresponds to spacing " "horizontal graphs, this corresponds to spacing "
"between columns.")) "between columns."))
menu.add_option(category, "ranksep", ranksep) menu.add_option(category, "ranksep", ranksep)
use_subgraphs = BooleanOption(_('Use subgraphs'), True) use_subgraphs = BooleanOption(_('Use subgraphs'), True)
use_subgraphs.set_help(_("Subgraphs can help Graphviz position " use_subgraphs.set_help(_("Subgraphs can help Graphviz position "
"spouses together, but with non-trivial " "spouses together, but with non-trivial "
"graphs will result in longer lines and " "graphs will result in longer lines and "
"larger graphs.")) "larger graphs."))
menu.add_option(category, "usesubgraphs", use_subgraphs) menu.add_option(category, "usesubgraphs", use_subgraphs)
################################ ################################
@@ -256,15 +255,15 @@ class GVOptions:
################################ ################################
note = TextOption(_("Note to add to the graph"), note = TextOption(_("Note to add to the graph"),
[""] ) [""])
note.set_help(_("This text will be added to the graph.")) note.set_help(_("This text will be added to the graph."))
menu.add_option(category, "note", note) menu.add_option(category, "note", note)
noteloc = EnumeratedListOption(_("Note location"), 't') noteloc = EnumeratedListOption(_("Note location"), 't')
for i in range( 0, len(_NOTELOC) ): for i in range(0, len(_NOTELOC)):
noteloc.add_item(_NOTELOC[i]["value"], _NOTELOC[i]["name"]) noteloc.add_item(_NOTELOC[i]["value"], _NOTELOC[i]["name"])
noteloc.set_help(_("Whether note will appear on top " noteloc.set_help(_("Whether note will appear on top "
"or bottom of the page.")) "or bottom of the page."))
menu.add_option(category, "noteloc", noteloc) menu.add_option(category, "noteloc", noteloc)
notesize = NumberOption(_("Note size"), 32, 8, 128) notesize = NumberOption(_("Note size"), 32, 8, 128)
@@ -278,17 +277,17 @@ class GVOptions:
pages are set to "1", then the page_dir control needs to pages are set to "1", then the page_dir control needs to
be unavailable be unavailable
""" """
if self.v_pages.get_value() > 1 or \ if self.v_pages.get_value() > 1 or self.h_pages.get_value() > 1:
self.h_pages.get_value() > 1:
self.page_dir.set_available(True) self.page_dir.set_available(True)
else: else:
self.page_dir.set_available(False) self.page_dir.set_available(False)
#-------------------------------------------------------------------------------
#------------------------------------------------------------------------------
# #
# GVDoc # GVDoc
# #
#------------------------------------------------------------------------------- #------------------------------------------------------------------------------
class GVDoc(metaclass=ABCMeta): class GVDoc(metaclass=ABCMeta):
""" """
Abstract Interface for Graphviz document generators. Output formats Abstract Interface for Graphviz document generators. Output formats
@@ -374,11 +373,12 @@ class GVDoc(metaclass=ABCMeta):
:return: nothing :return: nothing
""" """
#-------------------------------------------------------------------------------
#------------------------------------------------------------------------------
# #
# GVDocBase # GVDocBase
# #
#------------------------------------------------------------------------------- #------------------------------------------------------------------------------
class GVDocBase(BaseDoc, GVDoc): class GVDocBase(BaseDoc, GVDoc):
""" """
Base document generator for all Graphviz document generators. Classes that Base document generator for all Graphviz document generators. Classes that
@@ -388,40 +388,39 @@ class GVDocBase(BaseDoc, GVDoc):
def __init__(self, options, paper_style, uistate=None): def __init__(self, options, paper_style, uistate=None):
BaseDoc.__init__(self, None, paper_style, uistate=uistate) BaseDoc.__init__(self, None, paper_style, uistate=uistate)
self._filename = None self._filename = None
self._dot = BytesIO() self._dot = BytesIO()
self._paper = paper_style self._paper = paper_style
get_option_by_name = options.menu.get_option_by_name get_option = options.menu.get_option_by_name
get_value = lambda name: get_option_by_name(name).get_value()
self.dpi = get_value('dpi') self.dpi = get_option('dpi').get_value()
self.fontfamily = get_value('font_family') self.fontfamily = get_option('font_family').get_value()
self.fontsize = get_value('font_size') self.fontsize = get_option('font_size').get_value()
self.hpages = get_value('h_pages') self.hpages = get_option('h_pages').get_value()
self.nodesep = get_value('nodesep') self.nodesep = get_option('nodesep').get_value()
self.noteloc = get_value('noteloc') self.noteloc = get_option('noteloc').get_value()
self.notesize = get_value('notesize') self.notesize = get_option('notesize').get_value()
self.note = get_value('note') self.note = get_option('note').get_value()
self.pagedir = get_value('page_dir') self.pagedir = get_option('page_dir').get_value()
self.rankdir = get_value('rank_dir') self.rankdir = get_option('rank_dir').get_value()
self.ranksep = get_value('ranksep') self.ranksep = get_option('ranksep').get_value()
self.ratio = get_value('ratio') self.ratio = get_option('ratio').get_value()
self.vpages = get_value('v_pages') self.vpages = get_option('v_pages').get_value()
self.usesubgraphs = get_value('usesubgraphs') self.usesubgraphs = get_option('usesubgraphs').get_value()
self.spline = get_value('spline') self.spline = get_option('spline').get_value()
paper_size = paper_style.get_size() paper_size = paper_style.get_size()
# Subtract 0.01" from the drawing area to make some room between # Subtract 0.01" from the drawing area to make some room between
# this area and the margin in order to compensate for different # this area and the margin in order to compensate for different
# rounding errors internally in dot # rounding errors internally in dot
sizew = ( paper_size.get_width() - sizew = (paper_size.get_width() -
self._paper.get_left_margin() - self._paper.get_left_margin() -
self._paper.get_right_margin() ) / 2.54 - 0.01 self._paper.get_right_margin()) / 2.54 - 0.01
sizeh = ( paper_size.get_height() - sizeh = (paper_size.get_height() -
self._paper.get_top_margin() - self._paper.get_top_margin() -
self._paper.get_bottom_margin() ) / 2.54 - 0.01 self._paper.get_bottom_margin()) / 2.54 - 0.01
pheight = paper_size.get_height_inches() pheight = paper_size.get_height_inches()
pwidth = paper_size.get_width_inches() pwidth = paper_size.get_width_inches()
@@ -438,33 +437,33 @@ class GVDocBase(BaseDoc, GVDoc):
' bgcolor=white;\n' ' bgcolor=white;\n'
' center="true"; \n' ' center="true"; \n'
' charset="utf8";\n' ' charset="utf8";\n'
' concentrate="false";\n' + ' concentrate="false";\n' +
' dpi="%d";\n' % self.dpi + ' dpi="%d";\n' % self.dpi +
' graph [fontsize=%d];\n' % self.fontsize + ' graph [fontsize=%d];\n' % self.fontsize +
' margin="%3.2f,%3.2f"; \n' % (xmargin, ymargin) + ' margin="%3.2f,%3.2f"; \n' % (xmargin, ymargin) +
' mclimit="99";\n' + ' mclimit="99";\n' +
' nodesep="%.2f";\n' % self.nodesep + ' nodesep="%.2f";\n' % self.nodesep +
' outputorder="edgesfirst";\n' + ' outputorder="edgesfirst";\n' +
('#' if self.hpages == self.vpages == 1 else '') + ('#' if self.hpages == self.vpages == 1 else '') +
# comment out "page=" if the graph is on 1 page (bug #2121) # comment out "page=" if the graph is on 1 page (bug #2121)
' page="%3.2f,%3.2f";\n' % (pwidth, pheight) + ' page="%3.2f,%3.2f";\n' % (pwidth, pheight) +
' pagedir="%s";\n' % self.pagedir + ' pagedir="%s";\n' % self.pagedir +
' rankdir="%s";\n' % self.rankdir + ' rankdir="%s";\n' % self.rankdir +
' ranksep="%.2f";\n' % self.ranksep + ' ranksep="%.2f";\n' % self.ranksep +
' ratio="%s";\n' % self.ratio + ' ratio="%s";\n' % self.ratio +
' searchsize="100";\n' + ' searchsize="100";\n' +
' size="%3.2f,%3.2f"; \n' % (sizew, sizeh) + ' size="%3.2f,%3.2f"; \n' % (sizew, sizeh) +
' splines="%s";\n' % self.spline + ' splines="%s";\n' % self.spline +
'\n' + '\n' +
' edge [len=0.5 style=solid fontsize=%d];\n' % self.fontsize ' edge [len=0.5 style=solid fontsize=%d];\n' % self.fontsize)
)
if self.fontfamily: if self.fontfamily:
self.write( ' node [style=filled fontname="%s" fontsize=%d];\n' self.write(' node [style=filled fontname="%s" fontsize=%d];\n'
% ( self.fontfamily, self.fontsize ) ) % (self.fontfamily, self.fontsize))
else: else:
self.write( ' node [style=filled fontsize=%d];\n' self.write(' node [style=filled fontsize=%d];\n'
% self.fontsize ) % self.fontsize)
self.write( '\n' ) self.write('\n')
def write(self, text): def write(self, text):
""" Write text to the dot file """ """ Write text to the dot file """
@@ -482,10 +481,10 @@ class GVDocBase(BaseDoc, GVDoc):
if self.note: if self.note:
# build up the label # build up the label
label = '' label = ''
for line in self.note: # for every line in the note... for line in self.note: # for every line in the note...
line = line.strip() # ...strip whitespace from this line... line = line.strip() # ...strip whitespace from this line...
if line != '': # ...and if we still have a line... if line != '': # ...and if we still have a line...
if label != '': # ...see if we need to insert a newline... if label != '': # ...see if we need to insert a newline...
label += '\\n' label += '\\n'
label += line.replace('"', '\\\"') label += line.replace('"', '\\\"')
@@ -493,12 +492,11 @@ class GVDocBase(BaseDoc, GVDoc):
if label != '': if label != '':
self.write( self.write(
'\n' + '\n' +
' label="%s";\n' % label + ' label="%s";\n' % label +
' labelloc="%s";\n' % self.noteloc + ' labelloc="%s";\n' % self.noteloc +
' fontsize="%d";\n' % self.notesize ' fontsize="%d";\n' % self.notesize)
)
self.write( '}\n\n' ) self.write('}\n\n')
def add_node(self, node_id, label, shape="", color="", def add_node(self, node_id, label, shape="", color="",
style="", fillcolor="", url="", htmloutput=False): style="", fillcolor="", url="", htmloutput=False):
@@ -511,27 +509,27 @@ class GVDocBase(BaseDoc, GVDoc):
text = '[' text = '['
if shape: if shape:
text += ' shape="%s"' % shape text += ' shape="%s"' % shape
if color: if color:
text += ' color="%s"' % color text += ' color="%s"' % color
if fillcolor: if fillcolor:
text += ' fillcolor="%s"' % fillcolor text += ' fillcolor="%s"' % fillcolor
if style: if style:
text += ' style="%s"' % style text += ' style="%s"' % style
# note that we always output a label -- even if an empty string -- # note that we always output a label -- even if an empty string --
# otherwise Graphviz uses the node ID as the label which is unlikely # otherwise Graphviz uses the node ID as the label which is unlikely
# to be what the user wants to see in the graph # to be what the user wants to see in the graph
if label.startswith("<") or htmloutput: if label.startswith("<") or htmloutput:
text += ' label=<%s>' % label text += ' label=<%s>' % label
else: else:
text += ' label="%s"' % label text += ' label="%s"' % label
if url: if url:
text += ' URL="%s"' % url text += ' URL="%s"' % url
text += " ]" text += " ]"
self.write(' "%s" %s;\n' % (node_id, text)) self.write(' "%s" %s;\n' % (node_id, text))
@@ -590,22 +588,22 @@ class GVDocBase(BaseDoc, GVDoc):
def start_subgraph(self, graph_id): def start_subgraph(self, graph_id):
""" Implement GVDocBase.start_subgraph() """ """ Implement GVDocBase.start_subgraph() """
graph_id = graph_id.replace(' ', '_') # for user-defined ID with space graph_id = graph_id.replace(' ', '_') # for user-defined ID with space
self.write( self.write(
' subgraph cluster_%s\n' % graph_id + ' subgraph cluster_%s\n' % graph_id +
' {\n' + ' {\n' +
' style="invis";\n' # no border around subgraph (#0002176) ' style="invis";\n') # no border around subgraph (#0002176)
)
def end_subgraph(self): def end_subgraph(self):
""" Implement GVDocBase.end_subgraph() """ """ Implement GVDocBase.end_subgraph() """
self.write(' }\n') self.write(' }\n')
#-------------------------------------------------------------------------------
#------------------------------------------------------------------------------
# #
# GVDotDoc # GVDotDoc
# #
#------------------------------------------------------------------------------- #------------------------------------------------------------------------------
class GVDotDoc(GVDocBase): class GVDotDoc(GVDocBase):
""" GVDoc implementation that generates a .gv text file. """ """ GVDoc implementation that generates a .gv text file. """
@@ -620,11 +618,12 @@ class GVDotDoc(GVDocBase):
with open(self._filename, "wb") as dotfile: with open(self._filename, "wb") as dotfile:
dotfile.write(self._dot.getvalue()) dotfile.write(self._dot.getvalue())
#-------------------------------------------------------------------------------
#------------------------------------------------------------------------------
# #
# GVPsDoc # GVPsDoc
# #
#------------------------------------------------------------------------------- #------------------------------------------------------------------------------
class GVPsDoc(GVDocBase): class GVPsDoc(GVDocBase):
""" GVDoc implementation that generates a .ps file using Graphviz. """ """ GVDoc implementation that generates a .ps file using Graphviz. """
@@ -650,7 +649,7 @@ class GVPsDoc(GVDocBase):
self._filename += ".ps" self._filename += ".ps"
# Create a temporary dot file # Create a temporary dot file
(handle, tmp_dot) = tempfile.mkstemp(".gv" ) (handle, tmp_dot) = tempfile.mkstemp(".gv")
dotfile = os.fdopen(handle, "wb") dotfile = os.fdopen(handle, "wb")
dotfile.write(self._dot.getvalue()) dotfile.write(self._dot.getvalue())
dotfile.close() dotfile.close()
@@ -667,23 +666,28 @@ class GVPsDoc(GVDocBase):
# disappeared. I used 1 inch margins always. # disappeared. I used 1 inch margins always.
# See bug tracker issue 2815 # See bug tracker issue 2815
# :cairo does not work with Graphviz 2.26.3 and later See issue 4164 # :cairo does not work with Graphviz 2.26.3 and later See issue 4164
# recent versions of Graphviz doesn't even try, just puts out a single
# large page.
command = 'dot -Tps:cairo -o"%s" "%s"' % (self._filename, tmp_dot) command = 'dot -Tps:cairo -o"%s" "%s"' % (self._filename, tmp_dot)
dotversion = str(Popen(['dot', '-V'], stderr=PIPE).communicate(input=None)[1]) dotversion = str(Popen(['dot', '-V'],
# Problem with dot 2.26.3 and later and multiple pages, which gives "cairo: out of stderr=PIPE).communicate(input=None)[1])
# memory" If the :cairo is skipped for these cases it gives acceptable # Problem with dot 2.26.3 and later and multiple pages, which gives
# result. # "cairo: out of memory" If the :cairo is skipped for these cases it
if (dotversion.find('2.26.3') or dotversion.find('2.28.0') != -1) and (self.vpages * self.hpages) > 1: # gives bad result for non-Latin-1 characters (utf-8).
command = command.replace(':cairo','') if (dotversion.find('2.26.3') or dotversion.find('2.28.0') != -1) and \
(self.vpages * self.hpages) > 1:
command = command.replace(':cairo', '')
os.system(command) os.system(command)
# Delete the temporary dot file # Delete the temporary dot file
os.remove(tmp_dot) os.remove(tmp_dot)
#-------------------------------------------------------------------------------
#------------------------------------------------------------------------------
# #
# GVSvgDoc # GVSvgDoc
# #
#------------------------------------------------------------------------------- #------------------------------------------------------------------------------
class GVSvgDoc(GVDocBase): class GVSvgDoc(GVDocBase):
""" GVDoc implementation that generates a .svg file using Graphviz. """ """ GVDoc implementation that generates a .svg file using Graphviz. """
@@ -703,21 +707,22 @@ class GVSvgDoc(GVDocBase):
self._filename += ".svg" self._filename += ".svg"
# Create a temporary dot file # Create a temporary dot file
(handle, tmp_dot) = tempfile.mkstemp(".gv" ) (handle, tmp_dot) = tempfile.mkstemp(".gv")
dotfile = os.fdopen(handle, "wb") dotfile = os.fdopen(handle, "wb")
dotfile.write(self._dot.getvalue()) dotfile.write(self._dot.getvalue())
dotfile.close() dotfile.close()
# Generate the SVG file. # Generate the SVG file.
os.system( 'dot -Tsvg:cairo -o"%s" "%s"' % (self._filename, tmp_dot) ) os.system('dot -Tsvg:cairo -o"%s" "%s"' % (self._filename, tmp_dot))
# Delete the temporary dot file # Delete the temporary dot file
os.remove(tmp_dot) os.remove(tmp_dot)
#-------------------------------------------------------------------------------
#------------------------------------------------------------------------------
# #
# GVSvgzDoc # GVSvgzDoc
# #
#------------------------------------------------------------------------------- #------------------------------------------------------------------------------
class GVSvgzDoc(GVDocBase): class GVSvgzDoc(GVDocBase):
""" GVDoc implementation that generates a .svg file using Graphviz. """ """ GVDoc implementation that generates a .svg file using Graphviz. """
@@ -737,21 +742,22 @@ class GVSvgzDoc(GVDocBase):
self._filename += ".svgz" self._filename += ".svgz"
# Create a temporary dot file # Create a temporary dot file
(handle, tmp_dot) = tempfile.mkstemp(".gv" ) (handle, tmp_dot) = tempfile.mkstemp(".gv")
dotfile = os.fdopen(handle, "wb") dotfile = os.fdopen(handle, "wb")
dotfile.write(self._dot.getvalue()) dotfile.write(self._dot.getvalue())
dotfile.close() dotfile.close()
# Generate the SVGZ file. # Generate the SVGZ file.
os.system( 'dot -Tsvgz -o"%s" "%s"' % (self._filename, tmp_dot) ) os.system('dot -Tsvgz -o"%s" "%s"' % (self._filename, tmp_dot))
# Delete the temporary dot file # Delete the temporary dot file
os.remove(tmp_dot) os.remove(tmp_dot)
#-------------------------------------------------------------------------------
#------------------------------------------------------------------------------
# #
# GVPngDoc # GVPngDoc
# #
#------------------------------------------------------------------------------- #------------------------------------------------------------------------------
class GVPngDoc(GVDocBase): class GVPngDoc(GVDocBase):
""" GVDoc implementation that generates a .png file using Graphviz. """ """ GVDoc implementation that generates a .png file using Graphviz. """
@@ -771,21 +777,22 @@ class GVPngDoc(GVDocBase):
self._filename += ".png" self._filename += ".png"
# Create a temporary dot file # Create a temporary dot file
(handle, tmp_dot) = tempfile.mkstemp(".gv" ) (handle, tmp_dot) = tempfile.mkstemp(".gv")
dotfile = os.fdopen(handle, "wb") dotfile = os.fdopen(handle, "wb")
dotfile.write(self._dot.getvalue()) dotfile.write(self._dot.getvalue())
dotfile.close() dotfile.close()
# Generate the PNG file. # Generate the PNG file.
os.system( 'dot -Tpng -o"%s" "%s"' % (self._filename, tmp_dot) ) os.system('dot -Tpng -o"%s" "%s"' % (self._filename, tmp_dot))
# Delete the temporary dot file # Delete the temporary dot file
os.remove(tmp_dot) os.remove(tmp_dot)
#-------------------------------------------------------------------------------
#------------------------------------------------------------------------------
# #
# GVJpegDoc # GVJpegDoc
# #
#------------------------------------------------------------------------------- #------------------------------------------------------------------------------
class GVJpegDoc(GVDocBase): class GVJpegDoc(GVDocBase):
""" GVDoc implementation that generates a .jpg file using Graphviz. """ """ GVDoc implementation that generates a .jpg file using Graphviz. """
@@ -805,21 +812,22 @@ class GVJpegDoc(GVDocBase):
self._filename += ".jpg" self._filename += ".jpg"
# Create a temporary dot file # Create a temporary dot file
(handle, tmp_dot) = tempfile.mkstemp(".gv" ) (handle, tmp_dot) = tempfile.mkstemp(".gv")
dotfile = os.fdopen(handle, "wb") dotfile = os.fdopen(handle, "wb")
dotfile.write(self._dot.getvalue()) dotfile.write(self._dot.getvalue())
dotfile.close() dotfile.close()
# Generate the JPEG file. # Generate the JPEG file.
os.system( 'dot -Tjpg -o"%s" "%s"' % (self._filename, tmp_dot) ) os.system('dot -Tjpg -o"%s" "%s"' % (self._filename, tmp_dot))
# Delete the temporary dot file # Delete the temporary dot file
os.remove(tmp_dot) os.remove(tmp_dot)
#-------------------------------------------------------------------------------
#------------------------------------------------------------------------------
# #
# GVGifDoc # GVGifDoc
# #
#------------------------------------------------------------------------------- #------------------------------------------------------------------------------
class GVGifDoc(GVDocBase): class GVGifDoc(GVDocBase):
""" GVDoc implementation that generates a .gif file using Graphviz. """ """ GVDoc implementation that generates a .gif file using Graphviz. """
@@ -839,21 +847,22 @@ class GVGifDoc(GVDocBase):
self._filename += ".gif" self._filename += ".gif"
# Create a temporary dot file # Create a temporary dot file
(handle, tmp_dot) = tempfile.mkstemp(".gv" ) (handle, tmp_dot) = tempfile.mkstemp(".gv")
dotfile = os.fdopen(handle, "wb") dotfile = os.fdopen(handle, "wb")
dotfile.write(self._dot.getvalue()) dotfile.write(self._dot.getvalue())
dotfile.close() dotfile.close()
# Generate the GIF file. # Generate the GIF file.
os.system( 'dot -Tgif -o"%s" "%s"' % (self._filename, tmp_dot) ) os.system('dot -Tgif -o"%s" "%s"' % (self._filename, tmp_dot))
# Delete the temporary dot file # Delete the temporary dot file
os.remove(tmp_dot) os.remove(tmp_dot)
#-------------------------------------------------------------------------------
#------------------------------------------------------------------------------
# #
# GVPdfGvDoc # GVPdfGvDoc
# #
#------------------------------------------------------------------------------- #------------------------------------------------------------------------------
class GVPdfGvDoc(GVDocBase): class GVPdfGvDoc(GVDocBase):
""" GVDoc implementation that generates a .pdf file using Graphviz. """ """ GVDoc implementation that generates a .pdf file using Graphviz. """
@@ -876,23 +885,24 @@ class GVPdfGvDoc(GVDocBase):
self._filename += ".pdf" self._filename += ".pdf"
# Create a temporary dot file # Create a temporary dot file
(handle, tmp_dot) = tempfile.mkstemp(".gv" ) (handle, tmp_dot) = tempfile.mkstemp(".gv")
dotfile = os.fdopen(handle, "wb") dotfile = os.fdopen(handle, "wb")
dotfile.write(self._dot.getvalue()) dotfile.write(self._dot.getvalue())
dotfile.close() dotfile.close()
fname = self._filename fname = self._filename
# Generate the PDF file. # Generate the PDF file.
os.system( 'dot -Tpdf -o"%s" "%s"' % (fname, tmp_dot) ) os.system('dot -Tpdf -o"%s" "%s"' % (fname, tmp_dot))
# Delete the temporary dot file # Delete the temporary dot file
os.remove(tmp_dot) os.remove(tmp_dot)
#-------------------------------------------------------------------------------
#------------------------------------------------------------------------------
# #
# GVPdfGsDoc # GVPdfGsDoc
# #
#------------------------------------------------------------------------------- #------------------------------------------------------------------------------
class GVPdfGsDoc(GVDocBase): class GVPdfGsDoc(GVDocBase):
""" GVDoc implementation that generates a .pdf file using Ghostscript. """ """ GVDoc implementation that generates a .pdf file using Ghostscript. """
def __init__(self, options, paper_style): def __init__(self, options, paper_style):
@@ -910,103 +920,144 @@ class GVPdfGsDoc(GVDocBase):
self._filename += ".pdf" self._filename += ".pdf"
# Create a temporary dot file # Create a temporary dot file
(handle, tmp_dot) = tempfile.mkstemp(".gv" ) (handle, tmp_dot) = tempfile.mkstemp(".gv")
dotfile = os.fdopen(handle, "wb") dotfile = os.fdopen(handle, "wb")
dotfile.write(self._dot.getvalue()) dotfile.write(self._dot.getvalue())
dotfile.close() dotfile.close()
# Create a temporary PostScript file # Create a temporary PostScript file
(handle, tmp_ps) = tempfile.mkstemp(".ps" ) (handle, tmp_ps) = tempfile.mkstemp(".ps")
os.close( handle ) os.close(handle)
# Generate PostScript using dot # Generate PostScript using dot
# Reason for using -Tps:cairo. Needed for Non Latin-1 letters # Reason for using -Tps:cairo. Needed for Non Latin-1 letters
# See bug tracker issue 2815 # See bug tracker issue 2815
# :cairo does not work with Graphviz 2.26.3 and later See issue 4164 # :cairo does not work with with multi-page See issue 4164
# recent versions of Graphviz doesn't even try, just puts out a single
# large page, so we use Ghostscript to split it up.
command = 'dot -Tps:cairo -o"%s" "%s"' % ( tmp_ps, tmp_dot ) command = 'dot -Tps:cairo -o"%s" "%s"' % (tmp_ps, tmp_dot)
dotversion = str(Popen(['dot', '-V'], stderr=PIPE).communicate(input=None)[1])
# Problem with dot 2.26.3 and later and multiple pages, which gives "cairo: out
# of memory". If the :cairo is skipped for these cases it gives
# acceptable result.
if (dotversion.find('2.26.3') or dotversion.find('2.28.0') != -1) and (self.vpages * self.hpages) > 1:
command = command.replace(':cairo','')
os.system(command) os.system(command)
# Add .5 to remove rounding errors. # Add .5 to remove rounding errors.
paper_size = self._paper.get_size() paper_size = self._paper.get_size()
width_pt = int( (paper_size.get_width_inches() * 72) + 0.5 ) width_pt = int((paper_size.get_width_inches() * 72) + .5)
height_pt = int( (paper_size.get_height_inches() * 72) + 0.5 ) height_pt = int((paper_size.get_height_inches() * 72) + .5)
if (self.vpages * self.hpages) == 1:
# -dDEVICEWIDTHPOINTS=%d' -dDEVICEHEIGHTPOINTS=%d
command = '%s -q -sDEVICE=pdfwrite -dNOPAUSE '\
'-dDEVICEWIDTHPOINTS=%d -dDEVICEHEIGHTPOINTS=%d '\
'-sOutputFile="%s" "%s" -c quit' % (
_GS_CMD, width_pt, height_pt, self._filename, tmp_ps)
os.system(command)
os.remove(tmp_ps)
return
# Margins (in centimeters) to pixels 72/2.54=28.345
margin_t = int(28.345 * self._paper.get_top_margin())
margin_b = int(28.345 * self._paper.get_bottom_margin())
margin_r = int(28.345 * self._paper.get_right_margin())
margin_l = int(28.345 * self._paper.get_left_margin())
margin_x = margin_l + margin_r
margin_y = margin_t + margin_b
# Convert to PDF using ghostscript # Convert to PDF using ghostscript
command = '%s -q -sDEVICE=pdfwrite -dNOPAUSE -dDEVICEWIDTHPOINTS=%d' \ list_of_pieces = []
' -dDEVICEHEIGHTPOINTS=%d -sOutputFile="%s" "%s" -c quit' \
% ( _GS_CMD, width_pt, height_pt, self._filename, tmp_ps ) x_rng = range(1, self.hpages + 1) if 'L' in self.pagedir \
else range(self.hpages, 0, -1)
y_rng = range(1, self.vpages + 1) if 'B' in self.pagedir \
else range(self.vpages, 0, -1)
if self.pagedir[0] in 'TB':
the_list = ((__x, __y) for __y in y_rng for __x in x_rng)
else:
the_list = ((__x, __y) for __x in x_rng for __y in y_rng)
for __x, __y in the_list:
# Slit PS file to pieces of PDF
page_offset_x = (__x - 1) * (margin_x - width_pt)
page_offset_y = (__y - 1) * (margin_y - height_pt)
tmp_pdf_piece = "%s_%d_%d.pdf" % (tmp_ps, __x, __y)
list_of_pieces.append(tmp_pdf_piece)
# Generate Ghostscript code
command = '%s -q -dBATCH -dNOPAUSE -dSAFER -g%dx%d '\
'-sOutputFile="%s" -r72 -sDEVICE=pdfwrite '\
'-c "<</.HWMargins [%d %d %d %d] /PageOffset [%d %d]>> '\
'setpagedevice" -f "%s"' % (
_GS_CMD, width_pt + 10, height_pt + 10, tmp_pdf_piece,
margin_l, margin_b, margin_r, margin_t,
page_offset_x + 5, page_offset_y + 5, tmp_ps)
# Execute Ghostscript
os.system(command)
# Merge pieces to single multipage PDF ;
command = '%s -q -dBATCH -dNOPAUSE '\
'-sOUTPUTFILE=%s -r72 -sDEVICE=pdfwrite %s '\
% (_GS_CMD, self._filename, ' '.join(list_of_pieces))
os.system(command) os.system(command)
# Clean temporary files
os.remove(tmp_ps) os.remove(tmp_ps)
for tmp_pdf_piece in list_of_pieces:
os.remove(tmp_pdf_piece)
os.remove(tmp_dot) os.remove(tmp_dot)
#------------------------------------------------------------------------------- #------------------------------------------------------------------------------
# #
# Various Graphviz formats. # Various Graphviz formats.
# #
#------------------------------------------------------------------------------- #------------------------------------------------------------------------------
FORMATS = [] FORMATS = []
if _DOT_FOUND: if _DOT_FOUND:
if _GS_CMD != "": if _GS_CMD != "":
FORMATS += [{ 'type' : "gspdf", FORMATS += [{'type' : "gspdf",
'ext' : "pdf", 'ext' : "pdf",
'descr': _("PDF (Ghostscript)"), 'descr': _("PDF (Ghostscript)"),
'mime' : "application/pdf", 'mime' : "application/pdf",
'class': GVPdfGsDoc }] 'class': GVPdfGsDoc}]
FORMATS += [{ 'type' : "gvpdf", FORMATS += [{'type' : "gvpdf",
'ext' : "pdf", 'ext' : "pdf",
'descr': _("PDF (Graphviz)"), 'descr': _("PDF (Graphviz)"),
'mime' : "application/pdf", 'mime' : "application/pdf",
'class': GVPdfGvDoc }] 'class': GVPdfGvDoc}]
FORMATS += [{ 'type' : "ps", FORMATS += [{'type' : "ps",
'ext' : "ps", 'ext' : "ps",
'descr': _("PostScript"), 'descr': _("PostScript"),
'mime' : "application/postscript", 'mime' : "application/postscript",
'class': GVPsDoc }] 'class': GVPsDoc}]
FORMATS += [{ 'type' : "svg", FORMATS += [{'type' : "svg",
'ext' : "svg", 'ext' : "svg",
'descr': _("Structured Vector Graphics (SVG)"), 'descr': _("Structured Vector Graphics (SVG)"),
'mime' : "image/svg", 'mime' : "image/svg",
'class': GVSvgDoc }] 'class': GVSvgDoc}]
FORMATS += [{ 'type' : "svgz", FORMATS += [{'type' : "svgz",
'ext' : "svgz", 'ext' : "svgz",
'descr': _("Compressed Structured Vector Graphs (SVGZ)"), 'descr': _("Compressed Structured Vector Graphs (SVGZ)"),
'mime' : "image/svgz", 'mime' : "image/svgz",
'class': GVSvgzDoc }] 'class': GVSvgzDoc}]
FORMATS += [{ 'type' : "jpg", FORMATS += [{'type' : "jpg",
'ext' : "jpg", 'ext' : "jpg",
'descr': _("JPEG image"), 'descr': _("JPEG image"),
'mime' : "image/jpeg", 'mime' : "image/jpeg",
'class': GVJpegDoc }] 'class': GVJpegDoc}]
FORMATS += [{ 'type' : "gif", FORMATS += [{'type' : "gif",
'ext' : "gif", 'ext' : "gif",
'descr': _("GIF image"), 'descr': _("GIF image"),
'mime' : "image/gif", 'mime' : "image/gif",
'class': GVGifDoc }] 'class': GVGifDoc}]
FORMATS += [{ 'type' : "png", FORMATS += [{'type' : "png",
'ext' : "png", 'ext' : "png",
'descr': _("PNG image"), 'descr': _("PNG image"),
'mime' : "image/png", 'mime' : "image/png",
'class': GVPngDoc }] 'class': GVPngDoc}]
FORMATS += [{ 'type' : "dot", FORMATS += [{'type' : "dot",
'ext' : "gv", 'ext' : "gv",
'descr': _("Graphviz File"), 'descr': _("Graphviz File"),
'mime' : "text/x-graphviz", 'mime' : "text/x-graphviz",
'class': GVDotDoc }] 'class': GVDotDoc}]

View File

@@ -61,7 +61,7 @@ def find_file( filename):
try: try:
if os.path.isfile(filename): if os.path.isfile(filename):
return(filename) return(filename)
except UnicodeError: except UnicodeError as err:
LOG.error("Filename %s raised a Unicode Error %s.", repr(filename), err) LOG.error("Filename %s raised a Unicode Error %s.", repr(filename), err)
LOG.debug("Filename %s not found.", repr(filename)) LOG.debug("Filename %s not found.", repr(filename))
@@ -228,6 +228,24 @@ def search_for(name):
return 1 return 1
return 0 return 0
def where_is(name):
""" This command is similar to the Linux "whereis -b file" command.
It looks for an executable file (name) in the PATH python is using, as
well as several likely other paths. It returns the first file found,
or an empty string if not found.
"""
paths = set(os.environ['PATH'].split(os.pathsep))
if not win():
paths.update(("/bin", "/usr/bin", "/usr/local/bin", "/opt/local/bin",
"/opt/bin"))
for i in paths:
fname = os.path.join(i, name)
if os.access(fname, os.X_OK) and not os.path.isdir(fname):
return fname
return ""
def create_checksum(full_path): def create_checksum(full_path):
""" """
Create a md5 hash for the given file. Create a md5 hash for the given file.