diff --git a/gramps/gen/plug/docgen/graphdoc.py b/gramps/gen/plug/docgen/graphdoc.py index 694812168..31da27655 100644 --- a/gramps/gen/plug/docgen/graphdoc.py +++ b/gramps/gen/plug/docgen/graphdoc.py @@ -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,19 +36,18 @@ 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 + BooleanOption from ...constfunc import win #------------------------------------------------------------------------- @@ -55,41 +56,41 @@ 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" } ] +#------------------------------------------------------------------------- +_FONTS = [{'name' : _("Default"), 'value' : ""}, + {'name' : _("PostScript / Helvetica"), 'value' : "Helvetica"}, + {'name' : _("TrueType / FreeSans"), 'value' : "FreeSans"}] -_RANKDIR = [ { 'name' : _("Vertical (↓)"), 'value' : "TB" }, - { 'name' : _("Vertical (↑)"), 'value' : "BT" }, - { 'name' : _("Horizontal (→)"), 'value' : "LR" }, - { 'name' : _("Horizontal (←)"), 'value' : "RL" } ] +_RANKDIR = [{'name' : _("Vertical (↓)"), 'value' : "TB"}, + {'name' : _("Vertical (↑)"), 'value' : "BT"}, + {'name' : _("Horizontal (→)"), 'value' : "LR"}, + {'name' : _("Horizontal (←)"), 'value' : "RL"}] -_PAGEDIR = [ { 'name' : _("Bottom, left"), 'value' :"BL" }, - { 'name' : _("Bottom, right"), 'value' :"BR" }, - { 'name' : _("Top, left"), 'value' :"TL" }, - { 'name' : _("Top, Right"), 'value' :"TR" }, - { 'name' : _("Right, bottom"), 'value' :"RB" }, - { 'name' : _("Right, top"), 'value' :"RT" }, - { 'name' : _("Left, bottom"), 'value' :"LB" }, - { 'name' : _("Left, top"), 'value' :"LT" } ] +_PAGEDIR = [{'name' : _("Bottom, left"), 'value' : "BL"}, + {'name' : _("Bottom, right"), 'value' : "BR"}, + {'name' : _("Top, left"), 'value' : "TL"}, + {'name' : _("Top, Right"), 'value' : "TR"}, + {'name' : _("Right, bottom"), 'value' : "RB"}, + {'name' : _("Right, top"), 'value' : "RT"}, + {'name' : _("Left, bottom"), 'value' : "LB"}, + {'name' : _("Left, top"), 'value' : "LT"}] -_RATIO = [ { 'name' : _("Compress to minimal size"), 'value': "compress" }, - { 'name' : _("Fill the given area"), 'value': "fill" }, - { 'name' : _("Expand uniformly"), 'value': "expand" } ] +_RATIO = [{'name' : _("Compress to minimal size"), 'value': "compress"}, + {'name' : _("Fill the given area"), 'value': "fill"}, + {'name' : _("Expand uniformly"), 'value': "expand"}] -_NOTELOC = [ { 'name' : _("Top"), 'value' : "t" }, - { 'name' : _("Bottom"), 'value' : "b" }] +_NOTELOC = [{'name' : _("Top"), 'value' : "t"}, + {'name' : _("Bottom"), 'value' : "b"}] -_SPLINE = [ { 'name' : _("Straight"), 'value' : "false" }, - { 'name' : _("Curved"), 'value' : "true", }, - { 'name' : _("Orthogonal"), 'value' : 'ortho'} ] +_SPLINE = [{'name' : _("Straight"), 'value' : "false"}, + {'name' : _("Curved"), 'value' : "true", }, + {'name' : _("Orthogonal"), 'value' : 'ortho'}] if win(): _DOT_FOUND = search_for("dot.exe") @@ -102,26 +103,23 @@ 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 to configure the graph reports. """ def __init__(self): - self.h_pages = None - self.v_pages = None - self.page_dir = None + self.h_pages = None + self.v_pages = None + self.page_dir = None self.dpi = None def add_menu_options(self, menu): @@ -139,9 +137,9 @@ class GVOptions: for item in _FONTS: font_family.add_item(item["value"], item["name"]) font_family.set_help(_("Choose the font family. If international " - "characters don't show, use FreeSans font. " - "FreeSans is available from: " - "http://www.nongnu.org/freefont/")) + "characters don't show, use FreeSans font. " + "FreeSans is available from: " + "http://www.nongnu.org/freefont/")) menu.add_option(category, "font_family", font_family) font_size = NumberOption(_("Font size"), 14, 8, 128) @@ -188,9 +186,9 @@ class GVOptions: # the page direction option only makes sense when the # number of horizontal and/or vertical pages is > 1, # so we need to remember these 3 controls for later - self.h_pages = h_pages - self.v_pages = v_pages - self.page_dir = page_dir + self.h_pages = h_pages + self.v_pages = v_pages + self.page_dir = page_dir # the page direction option only makes sense when the # number of horizontal and/or vertical pages is > 1 @@ -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 ' @@ -221,34 +220,34 @@ class GVOptions: menu.add_option(category, "ratio", aspect_ratio) dpi = NumberOption(_("DPI"), 75, 20, 1200) - dpi.set_help(_( "Dots per inch. When creating images such as " - ".gif or .png files for the web, try numbers " - "such as 100 or 300 DPI. PostScript and PDF files " - "always use 72 DPI.")) + dpi.set_help(_("Dots per inch. When creating images such as " + ".gif or .png files for the web, try numbers " + "such as 100 or 300 DPI. PostScript and PDF files " + "always use 72 DPI.")) menu.add_option(category, "dpi", dpi) self.dpi = dpi nodesep = NumberOption(_("Node spacing"), 0.20, 0.01, 5.00, 0.01) - nodesep.set_help(_( "The minimum amount of free space, in inches, " - "between individual nodes. For vertical graphs, " - "this corresponds to spacing between columns. " - "For horizontal graphs, this corresponds to " - "spacing between rows.")) + nodesep.set_help(_("The minimum amount of free space, in inches, " + "between individual nodes. For vertical graphs, " + "this corresponds to spacing between columns. " + "For horizontal graphs, this corresponds to " + "spacing between rows.")) menu.add_option(category, "nodesep", nodesep) ranksep = NumberOption(_("Rank spacing"), 0.20, 0.01, 5.00, 0.01) - ranksep.set_help(_( "The minimum amount of free space, in inches, " - "between ranks. For vertical graphs, this " - "corresponds to spacing between rows. For " - "horizontal graphs, this corresponds to spacing " - "between columns.")) + ranksep.set_help(_("The minimum amount of free space, in inches, " + "between ranks. For vertical graphs, this " + "corresponds to spacing between rows. For " + "horizontal graphs, this corresponds to spacing " + "between columns.")) menu.add_option(category, "ranksep", ranksep) use_subgraphs = BooleanOption(_('Use subgraphs'), True) use_subgraphs.set_help(_("Subgraphs can help Graphviz position " - "spouses together, but with non-trivial " - "graphs will result in longer lines and " - "larger graphs.")) + "spouses together, but with non-trivial " + "graphs will result in longer lines and " + "larger graphs.")) menu.add_option(category, "usesubgraphs", use_subgraphs) ################################ @@ -256,15 +255,15 @@ class GVOptions: ################################ note = TextOption(_("Note to add to the graph"), - [""] ) + [""]) note.set_help(_("This text will be added to the graph.")) menu.add_option(category, "note", note) 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.set_help(_("Whether note will appear on top " - "or bottom of the page.")) + "or bottom of the page.")) menu.add_option(category, "noteloc", noteloc) 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 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 @@ -388,40 +388,39 @@ class GVDocBase(BaseDoc, GVDoc): def __init__(self, options, paper_style, uistate=None): BaseDoc.__init__(self, None, paper_style, uistate=uistate) - self._filename = None + self._filename = None self._dot = BytesIO() - self._paper = paper_style + 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() + paper_size = paper_style.get_size() # Subtract 0.01" from the drawing area to make some room between # this area and the margin in order to compensate for different # rounding errors internally in dot - sizew = ( paper_size.get_width() - - self._paper.get_left_margin() - - self._paper.get_right_margin() ) / 2.54 - 0.01 - sizeh = ( paper_size.get_height() - - self._paper.get_top_margin() - - self._paper.get_bottom_margin() ) / 2.54 - 0.01 + sizew = (paper_size.get_width() - + self._paper.get_left_margin() - + self._paper.get_right_margin()) / 2.54 - 0.01 + sizeh = (paper_size.get_height() - + self._paper.get_top_margin() - + self._paper.get_bottom_margin()) / 2.54 - 0.01 pheight = paper_size.get_height_inches() pwidth = paper_size.get_width_inches() @@ -438,33 +437,33 @@ class GVDocBase(BaseDoc, GVDoc): ' bgcolor=white;\n' ' center="true"; \n' ' charset="utf8";\n' - ' concentrate="false";\n' + - ' dpi="%d";\n' % self.dpi + - ' graph [fontsize=%d];\n' % self.fontsize + - ' margin="%3.2f,%3.2f"; \n' % (xmargin, ymargin) + - ' mclimit="99";\n' + - ' nodesep="%.2f";\n' % self.nodesep + + ' concentrate="false";\n' + + ' dpi="%d";\n' % self.dpi + + ' graph [fontsize=%d];\n' % self.fontsize + + ' margin="%3.2f,%3.2f"; \n' % (xmargin, ymargin) + + ' mclimit="99";\n' + + ' nodesep="%.2f";\n' % self.nodesep + ' outputorder="edgesfirst";\n' + ('#' if self.hpages == self.vpages == 1 else '') + - # comment out "page=" if the graph is on 1 page (bug #2121) - ' page="%3.2f,%3.2f";\n' % (pwidth, pheight) + - ' pagedir="%s";\n' % self.pagedir + - ' rankdir="%s";\n' % self.rankdir + - ' ranksep="%.2f";\n' % self.ranksep + - ' ratio="%s";\n' % self.ratio + - ' searchsize="100";\n' + - ' 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 - ) + # comment out "page=" if the graph is on 1 page (bug #2121) + ' page="%3.2f,%3.2f";\n' % (pwidth, pheight) + + ' pagedir="%s";\n' % self.pagedir + + ' rankdir="%s";\n' % self.rankdir + + ' ranksep="%.2f";\n' % self.ranksep + + ' ratio="%s";\n' % self.ratio + + ' searchsize="100";\n' + + ' 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) + if self.fontfamily: - self.write( ' node [style=filled fontname="%s" fontsize=%d];\n' - % ( self.fontfamily, self.fontsize ) ) + self.write(' node [style=filled fontname="%s" fontsize=%d];\n' + % (self.fontfamily, self.fontsize)) else: - self.write( ' node [style=filled fontsize=%d];\n' - % self.fontsize ) - self.write( '\n' ) + self.write(' node [style=filled fontsize=%d];\n' + % self.fontsize) + self.write('\n') def write(self, text): """ Write text to the dot file """ @@ -482,10 +481,10 @@ class GVDocBase(BaseDoc, GVDoc): if self.note: # build up the label label = '' - for line in self.note: # for every line in the note... - line = line.strip() # ...strip whitespace from this line... - if line != '': # ...and if we still have a line... - if label != '': # ...see if we need to insert a newline... + for line in self.note: # for every line in the note... + line = line.strip() # ...strip whitespace from this line... + if line != '': # ...and if we still have a line... + if label != '': # ...see if we need to insert a newline... label += '\\n' label += line.replace('"', '\\\"') @@ -493,12 +492,11 @@ class GVDocBase(BaseDoc, GVDoc): if label != '': self.write( '\n' + - ' label="%s";\n' % label + - ' labelloc="%s";\n' % self.noteloc + - ' fontsize="%d";\n' % self.notesize - ) + ' label="%s";\n' % label + + ' labelloc="%s";\n' % self.noteloc + + ' fontsize="%d";\n' % self.notesize) - self.write( '}\n\n' ) + self.write('}\n\n') def add_node(self, node_id, label, shape="", color="", style="", fillcolor="", url="", htmloutput=False): @@ -511,27 +509,27 @@ class GVDocBase(BaseDoc, GVDoc): text = '[' if shape: - text += ' shape="%s"' % shape + text += ' shape="%s"' % shape if color: - text += ' color="%s"' % color + text += ' color="%s"' % color if fillcolor: - text += ' fillcolor="%s"' % fillcolor + text += ' fillcolor="%s"' % fillcolor if style: - text += ' style="%s"' % style + text += ' style="%s"' % style # note that we always output a label -- even if an empty string -- # otherwise Graphviz uses the node ID as the label which is unlikely # to be what the user wants to see in the graph if label.startswith("<") or htmloutput: - text += ' label=<%s>' % label + text += ' label=<%s>' % label else: - text += ' label="%s"' % label + text += ' label="%s"' % label if url: - text += ' URL="%s"' % url + text += ' URL="%s"' % url text += " ]" self.write(' "%s" %s;\n' % (node_id, text)) @@ -590,22 +588,22 @@ class GVDocBase(BaseDoc, GVDoc): def start_subgraph(self, graph_id): """ 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( ' 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. """ @@ -650,7 +649,7 @@ class GVPsDoc(GVDocBase): self._filename += ".ps" # Create a temporary dot file - (handle, tmp_dot) = tempfile.mkstemp(".gv" ) + (handle, tmp_dot) = tempfile.mkstemp(".gv") dotfile = os.fdopen(handle, "wb") dotfile.write(self._dot.getvalue()) dotfile.close() @@ -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: - command = command.replace(':cairo','') + 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. """ @@ -703,21 +707,22 @@ class GVSvgDoc(GVDocBase): self._filename += ".svg" # Create a temporary dot file - (handle, tmp_dot) = tempfile.mkstemp(".gv" ) + (handle, tmp_dot) = tempfile.mkstemp(".gv") dotfile = os.fdopen(handle, "wb") dotfile.write(self._dot.getvalue()) dotfile.close() # 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 os.remove(tmp_dot) -#------------------------------------------------------------------------------- + +#------------------------------------------------------------------------------ # # GVSvgzDoc # -#------------------------------------------------------------------------------- +#------------------------------------------------------------------------------ class GVSvgzDoc(GVDocBase): """ GVDoc implementation that generates a .svg file using Graphviz. """ @@ -737,21 +742,22 @@ class GVSvgzDoc(GVDocBase): self._filename += ".svgz" # Create a temporary dot file - (handle, tmp_dot) = tempfile.mkstemp(".gv" ) + (handle, tmp_dot) = tempfile.mkstemp(".gv") dotfile = os.fdopen(handle, "wb") dotfile.write(self._dot.getvalue()) dotfile.close() # 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 os.remove(tmp_dot) -#------------------------------------------------------------------------------- + +#------------------------------------------------------------------------------ # # GVPngDoc # -#------------------------------------------------------------------------------- +#------------------------------------------------------------------------------ class GVPngDoc(GVDocBase): """ GVDoc implementation that generates a .png file using Graphviz. """ @@ -771,21 +777,22 @@ class GVPngDoc(GVDocBase): self._filename += ".png" # Create a temporary dot file - (handle, tmp_dot) = tempfile.mkstemp(".gv" ) + (handle, tmp_dot) = tempfile.mkstemp(".gv") dotfile = os.fdopen(handle, "wb") dotfile.write(self._dot.getvalue()) dotfile.close() # 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 os.remove(tmp_dot) -#------------------------------------------------------------------------------- + +#------------------------------------------------------------------------------ # # GVJpegDoc # -#------------------------------------------------------------------------------- +#------------------------------------------------------------------------------ class GVJpegDoc(GVDocBase): """ GVDoc implementation that generates a .jpg file using Graphviz. """ @@ -805,21 +812,22 @@ class GVJpegDoc(GVDocBase): self._filename += ".jpg" # Create a temporary dot file - (handle, tmp_dot) = tempfile.mkstemp(".gv" ) + (handle, tmp_dot) = tempfile.mkstemp(".gv") dotfile = os.fdopen(handle, "wb") dotfile.write(self._dot.getvalue()) dotfile.close() # 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 os.remove(tmp_dot) -#------------------------------------------------------------------------------- + +#------------------------------------------------------------------------------ # # GVGifDoc # -#------------------------------------------------------------------------------- +#------------------------------------------------------------------------------ class GVGifDoc(GVDocBase): """ GVDoc implementation that generates a .gif file using Graphviz. """ @@ -839,21 +847,22 @@ class GVGifDoc(GVDocBase): self._filename += ".gif" # Create a temporary dot file - (handle, tmp_dot) = tempfile.mkstemp(".gv" ) + (handle, tmp_dot) = tempfile.mkstemp(".gv") dotfile = os.fdopen(handle, "wb") dotfile.write(self._dot.getvalue()) dotfile.close() # 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 os.remove(tmp_dot) -#------------------------------------------------------------------------------- + +#------------------------------------------------------------------------------ # # GVPdfGvDoc # -#------------------------------------------------------------------------------- +#------------------------------------------------------------------------------ class GVPdfGvDoc(GVDocBase): """ GVDoc implementation that generates a .pdf file using Graphviz. """ @@ -876,23 +885,24 @@ class GVPdfGvDoc(GVDocBase): self._filename += ".pdf" # Create a temporary dot file - (handle, tmp_dot) = tempfile.mkstemp(".gv" ) + (handle, tmp_dot) = tempfile.mkstemp(".gv") dotfile = os.fdopen(handle, "wb") dotfile.write(self._dot.getvalue()) dotfile.close() fname = self._filename # 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 os.remove(tmp_dot) -#------------------------------------------------------------------------------- + +#------------------------------------------------------------------------------ # # GVPdfGsDoc # -#------------------------------------------------------------------------------- +#------------------------------------------------------------------------------ class GVPdfGsDoc(GVDocBase): """ GVDoc implementation that generates a .pdf file using Ghostscript. """ def __init__(self, options, paper_style): @@ -910,103 +920,144 @@ class GVPdfGsDoc(GVDocBase): self._filename += ".pdf" # Create a temporary dot file - (handle, tmp_dot) = tempfile.mkstemp(".gv" ) + (handle, tmp_dot) = tempfile.mkstemp(".gv") dotfile = os.fdopen(handle, "wb") dotfile.write(self._dot.getvalue()) dotfile.close() # Create a temporary PostScript file - (handle, tmp_ps) = tempfile.mkstemp(".ps" ) - os.close( handle ) + (handle, tmp_ps) = tempfile.mkstemp(".ps") + os.close(handle) # 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','') + command = 'dot -Tps:cairo -o"%s" "%s"' % (tmp_ps, tmp_dot) 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 "<> '\ + '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: if _GS_CMD != "": - FORMATS += [{ 'type' : "gspdf", - 'ext' : "pdf", - 'descr': _("PDF (Ghostscript)"), - 'mime' : "application/pdf", - 'class': GVPdfGsDoc }] + FORMATS += [{'type' : "gspdf", + 'ext' : "pdf", + 'descr': _("PDF (Ghostscript)"), + 'mime' : "application/pdf", + 'class': GVPdfGsDoc}] - FORMATS += [{ 'type' : "gvpdf", - 'ext' : "pdf", - 'descr': _("PDF (Graphviz)"), - 'mime' : "application/pdf", - 'class': GVPdfGvDoc }] + FORMATS += [{'type' : "gvpdf", + 'ext' : "pdf", + 'descr': _("PDF (Graphviz)"), + 'mime' : "application/pdf", + 'class': GVPdfGvDoc}] - FORMATS += [{ 'type' : "ps", - 'ext' : "ps", - 'descr': _("PostScript"), - 'mime' : "application/postscript", - 'class': GVPsDoc }] + FORMATS += [{'type' : "ps", + 'ext' : "ps", + 'descr': _("PostScript"), + 'mime' : "application/postscript", + 'class': GVPsDoc}] - FORMATS += [{ 'type' : "svg", - 'ext' : "svg", - 'descr': _("Structured Vector Graphics (SVG)"), - 'mime' : "image/svg", - 'class': GVSvgDoc }] + FORMATS += [{'type' : "svg", + 'ext' : "svg", + 'descr': _("Structured Vector Graphics (SVG)"), + 'mime' : "image/svg", + 'class': GVSvgDoc}] - FORMATS += [{ 'type' : "svgz", - 'ext' : "svgz", - 'descr': _("Compressed Structured Vector Graphs (SVGZ)"), - 'mime' : "image/svgz", - 'class': GVSvgzDoc }] + FORMATS += [{'type' : "svgz", + 'ext' : "svgz", + 'descr': _("Compressed Structured Vector Graphs (SVGZ)"), + 'mime' : "image/svgz", + 'class': GVSvgzDoc}] - FORMATS += [{ 'type' : "jpg", - 'ext' : "jpg", - 'descr': _("JPEG image"), - 'mime' : "image/jpeg", - 'class': GVJpegDoc }] + FORMATS += [{'type' : "jpg", + 'ext' : "jpg", + 'descr': _("JPEG image"), + 'mime' : "image/jpeg", + 'class': GVJpegDoc}] - FORMATS += [{ 'type' : "gif", - 'ext' : "gif", - 'descr': _("GIF image"), - 'mime' : "image/gif", - 'class': GVGifDoc }] + FORMATS += [{'type' : "gif", + 'ext' : "gif", + 'descr': _("GIF image"), + 'mime' : "image/gif", + 'class': GVGifDoc}] - FORMATS += [{ 'type' : "png", - 'ext' : "png", - 'descr': _("PNG image"), - 'mime' : "image/png", - 'class': GVPngDoc }] + FORMATS += [{'type' : "png", + 'ext' : "png", + 'descr': _("PNG image"), + 'mime' : "image/png", + 'class': GVPngDoc}] -FORMATS += [{ 'type' : "dot", - 'ext' : "gv", - 'descr': _("Graphviz File"), - 'mime' : "text/x-graphviz", - 'class': GVDotDoc }] +FORMATS += [{'type' : "dot", + 'ext' : "gv", + 'descr': _("Graphviz File"), + 'mime' : "text/x-graphviz", + 'class': GVDotDoc}] diff --git a/gramps/gen/utils/file.py b/gramps/gen/utils/file.py index c819059a4..02bcb5c5e 100644 --- a/gramps/gen/utils/file.py +++ b/gramps/gen/utils/file.py @@ -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.