Merge pull request #472 from prculley/bug9783

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

View File

@ -8,6 +8,8 @@
# Copyright (C) 2007 Brian G. Matherly
# Copyright (C) 2009 Benny Malengier
# 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
# 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
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
""" Graphviz adapter for Graphs """
#-------------------------------------------------------------------------
#
# Standard Python modules
@ -34,16 +36,15 @@ import os
from io import BytesIO
import tempfile
from subprocess import Popen, PIPE
import sys
#-------------------------------------------------------------------------------
#-------------------------------------------------------------------------
#
# Gramps modules
#
#-------------------------------------------------------------------------------
#-------------------------------------------------------------------------
from ...const import GRAMPS_LOCALE as glocale
_ = glocale.translation.gettext
from ...utils.file import search_for
from ...utils.file import search_for, where_is
from . import BaseDoc
from ..menu import NumberOption, TextOption, EnumeratedListOption, \
BooleanOption
@ -55,13 +56,13 @@ from ...constfunc import win
#
#-------------------------------------------------------------------------
import logging
log = logging.getLogger(".graphdoc")
LOG = logging.getLogger(".graphdoc")
#-------------------------------------------------------------------------------
#-------------------------------------------------------------------------
#
# Private Constants
#
#-------------------------------------------------------------------------------
#-------------------------------------------------------------------------
_FONTS = [{'name' : _("Default"), 'value' : ""},
{'name' : _("PostScript / Helvetica"), 'value' : "Helvetica"},
{'name' : _("TrueType / FreeSans"), 'value' : "FreeSans"}]
@ -102,17 +103,14 @@ if win():
_GS_CMD = ""
else:
_DOT_FOUND = search_for("dot")
_GS_CMD = where_is("gs")
if search_for("gs") == 1:
_GS_CMD = "gs"
else:
_GS_CMD = ""
#-------------------------------------------------------------------------------
#------------------------------------------------------------------------------
#
# GVOptions
#
#-------------------------------------------------------------------------------
#------------------------------------------------------------------------------
class GVOptions:
"""
Defines all of the controls necessary
@ -204,7 +202,8 @@ class GVOptions:
aspect_ratio = EnumeratedListOption(_("Aspect ratio"), "fill")
for item in _RATIO:
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'
' Compress will not change the node spacing. \n'
' Fill will increase the node spacing to fit the print area in '
@ -278,17 +277,17 @@ class GVOptions:
pages are set to "1", then the page_dir control needs to
be unavailable
"""
if self.v_pages.get_value() > 1 or \
self.h_pages.get_value() > 1:
if self.v_pages.get_value() > 1 or self.h_pages.get_value() > 1:
self.page_dir.set_available(True)
else:
self.page_dir.set_available(False)
#-------------------------------------------------------------------------------
#------------------------------------------------------------------------------
#
# GVDoc
#
#-------------------------------------------------------------------------------
#------------------------------------------------------------------------------
class GVDoc(metaclass=ABCMeta):
"""
Abstract Interface for Graphviz document generators. Output formats
@ -374,11 +373,12 @@ class GVDoc(metaclass=ABCMeta):
:return: nothing
"""
#-------------------------------------------------------------------------------
#------------------------------------------------------------------------------
#
# GVDocBase
#
#-------------------------------------------------------------------------------
#------------------------------------------------------------------------------
class GVDocBase(BaseDoc, GVDoc):
"""
Base document generator for all Graphviz document generators. Classes that
@ -392,24 +392,23 @@ class GVDocBase(BaseDoc, GVDoc):
self._dot = BytesIO()
self._paper = paper_style
get_option_by_name = options.menu.get_option_by_name
get_value = lambda name: get_option_by_name(name).get_value()
get_option = options.menu.get_option_by_name
self.dpi = get_value('dpi')
self.fontfamily = get_value('font_family')
self.fontsize = get_value('font_size')
self.hpages = get_value('h_pages')
self.nodesep = get_value('nodesep')
self.noteloc = get_value('noteloc')
self.notesize = get_value('notesize')
self.note = get_value('note')
self.pagedir = get_value('page_dir')
self.rankdir = get_value('rank_dir')
self.ranksep = get_value('ranksep')
self.ratio = get_value('ratio')
self.vpages = get_value('v_pages')
self.usesubgraphs = get_value('usesubgraphs')
self.spline = get_value('spline')
self.dpi = get_option('dpi').get_value()
self.fontfamily = get_option('font_family').get_value()
self.fontsize = get_option('font_size').get_value()
self.hpages = get_option('h_pages').get_value()
self.nodesep = get_option('nodesep').get_value()
self.noteloc = get_option('noteloc').get_value()
self.notesize = get_option('notesize').get_value()
self.note = get_option('note').get_value()
self.pagedir = get_option('page_dir').get_value()
self.rankdir = get_option('rank_dir').get_value()
self.ranksep = get_option('ranksep').get_value()
self.ratio = get_option('ratio').get_value()
self.vpages = get_option('v_pages').get_value()
self.usesubgraphs = get_option('usesubgraphs').get_value()
self.spline = get_option('spline').get_value()
paper_size = paper_style.get_size()
@ -456,8 +455,8 @@ class GVDocBase(BaseDoc, GVDoc):
' size="%3.2f,%3.2f"; \n' % (sizew, sizeh) +
' splines="%s";\n' % self.spline +
'\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:
self.write(' node [style=filled fontname="%s" fontsize=%d];\n'
% (self.fontfamily, self.fontsize))
@ -495,8 +494,7 @@ class GVDocBase(BaseDoc, GVDoc):
'\n' +
' label="%s";\n' % label +
' labelloc="%s";\n' % self.noteloc +
' fontsize="%d";\n' % self.notesize
)
' fontsize="%d";\n' % self.notesize)
self.write('}\n\n')
@ -594,18 +592,18 @@ class GVDocBase(BaseDoc, GVDoc):
self.write(
' subgraph cluster_%s\n' % graph_id +
' {\n' +
' style="invis";\n' # no border around subgraph (#0002176)
)
' style="invis";\n') # no border around subgraph (#0002176)
def end_subgraph(self):
""" Implement GVDocBase.end_subgraph() """
self.write(' }\n')
#-------------------------------------------------------------------------------
#------------------------------------------------------------------------------
#
# GVDotDoc
#
#-------------------------------------------------------------------------------
#------------------------------------------------------------------------------
class GVDotDoc(GVDocBase):
""" GVDoc implementation that generates a .gv text file. """
@ -620,11 +618,12 @@ class GVDotDoc(GVDocBase):
with open(self._filename, "wb") as dotfile:
dotfile.write(self._dot.getvalue())
#-------------------------------------------------------------------------------
#------------------------------------------------------------------------------
#
# GVPsDoc
#
#-------------------------------------------------------------------------------
#------------------------------------------------------------------------------
class GVPsDoc(GVDocBase):
""" GVDoc implementation that generates a .ps file using Graphviz. """
@ -667,23 +666,28 @@ class GVPsDoc(GVDocBase):
# disappeared. I used 1 inch margins always.
# See bug tracker issue 2815
# :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)
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:
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 bad result for non-Latin-1 characters (utf-8).
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)
# Delete the temporary dot file
os.remove(tmp_dot)
#-------------------------------------------------------------------------------
#------------------------------------------------------------------------------
#
# GVSvgDoc
#
#-------------------------------------------------------------------------------
#------------------------------------------------------------------------------
class GVSvgDoc(GVDocBase):
""" GVDoc implementation that generates a .svg file using Graphviz. """
@ -713,11 +717,12 @@ class GVSvgDoc(GVDocBase):
# Delete the temporary dot file
os.remove(tmp_dot)
#-------------------------------------------------------------------------------
#------------------------------------------------------------------------------
#
# GVSvgzDoc
#
#-------------------------------------------------------------------------------
#------------------------------------------------------------------------------
class GVSvgzDoc(GVDocBase):
""" GVDoc implementation that generates a .svg file using Graphviz. """
@ -747,11 +752,12 @@ class GVSvgzDoc(GVDocBase):
# Delete the temporary dot file
os.remove(tmp_dot)
#-------------------------------------------------------------------------------
#------------------------------------------------------------------------------
#
# GVPngDoc
#
#-------------------------------------------------------------------------------
#------------------------------------------------------------------------------
class GVPngDoc(GVDocBase):
""" GVDoc implementation that generates a .png file using Graphviz. """
@ -781,11 +787,12 @@ class GVPngDoc(GVDocBase):
# Delete the temporary dot file
os.remove(tmp_dot)
#-------------------------------------------------------------------------------
#------------------------------------------------------------------------------
#
# GVJpegDoc
#
#-------------------------------------------------------------------------------
#------------------------------------------------------------------------------
class GVJpegDoc(GVDocBase):
""" GVDoc implementation that generates a .jpg file using Graphviz. """
@ -815,11 +822,12 @@ class GVJpegDoc(GVDocBase):
# Delete the temporary dot file
os.remove(tmp_dot)
#-------------------------------------------------------------------------------
#------------------------------------------------------------------------------
#
# GVGifDoc
#
#-------------------------------------------------------------------------------
#------------------------------------------------------------------------------
class GVGifDoc(GVDocBase):
""" GVDoc implementation that generates a .gif file using Graphviz. """
@ -849,11 +857,12 @@ class GVGifDoc(GVDocBase):
# Delete the temporary dot file
os.remove(tmp_dot)
#-------------------------------------------------------------------------------
#------------------------------------------------------------------------------
#
# GVPdfGvDoc
#
#-------------------------------------------------------------------------------
#------------------------------------------------------------------------------
class GVPdfGvDoc(GVDocBase):
""" GVDoc implementation that generates a .pdf file using Graphviz. """
@ -888,11 +897,12 @@ class GVPdfGvDoc(GVDocBase):
# Delete the temporary dot file
os.remove(tmp_dot)
#-------------------------------------------------------------------------------
#------------------------------------------------------------------------------
#
# GVPdfGsDoc
#
#-------------------------------------------------------------------------------
#------------------------------------------------------------------------------
class GVPdfGsDoc(GVDocBase):
""" GVDoc implementation that generates a .pdf file using Ghostscript. """
def __init__(self, options, paper_style):
@ -922,36 +932,77 @@ class GVPdfGsDoc(GVDocBase):
# Generate PostScript using dot
# Reason for using -Tps:cairo. Needed for Non Latin-1 letters
# 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)
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)
# Add .5 to remove rounding errors.
paper_size = self._paper.get_size()
width_pt = int( (paper_size.get_width_inches() * 72) + 0.5 )
height_pt = int( (paper_size.get_height_inches() * 72) + 0.5 )
width_pt = int((paper_size.get_width_inches() * 72) + .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
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 )
list_of_pieces = []
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)
# Clean temporary files
os.remove(tmp_ps)
for tmp_pdf_piece in list_of_pieces:
os.remove(tmp_pdf_piece)
os.remove(tmp_dot)
#-------------------------------------------------------------------------------
#------------------------------------------------------------------------------
#
# Various Graphviz formats.
#
#-------------------------------------------------------------------------------
#------------------------------------------------------------------------------
FORMATS = []
if _DOT_FOUND:

View File

@ -61,7 +61,7 @@ def find_file( filename):
try:
if os.path.isfile(filename):
return(filename)
except UnicodeError:
except UnicodeError as err:
LOG.error("Filename %s raised a Unicode Error %s.", repr(filename), err)
LOG.debug("Filename %s not found.", repr(filename))
@ -228,6 +228,24 @@ def search_for(name):
return 1
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):
"""
Create a md5 hash for the given file.