#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2000-2007 Donald N. Allingham
# Copyright (C) 2002 Gary Shao
# Copyright (C) 2007 Brian G. Matherly
# Copyright (C) 2009 Benny Malengier
# Copyright (C) 2009 Gary Burton
# Copyright (C) 2014 Nick Hall
# Copyright (C) 2017 Paul Franklin
#
# 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
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
#-------------------------------------------------------------------------
#
# standard python modules
#
#-------------------------------------------------------------------------
import os
from xml.sax.saxutils import escape
[docs]
def escxml(string):
"""
Escapes XML special characters.
"""
return escape(string, { '"' : '"' } )
#-------------------------------------------------------------------------
#
# Gramps modules
#
#-------------------------------------------------------------------------
from .paragraphstyle import ParagraphStyle
from .fontstyle import FontStyle
from .tablestyle import TableStyle, TableCellStyle
from .graphicstyle import GraphicsStyle
#-------------------------------------------------------------------------
#
# set up logging
#
#-------------------------------------------------------------------------
import logging
log = logging.getLogger(".stylesheet")
#-------------------------------------------------------------------------
#
# SAX interface
#
#-------------------------------------------------------------------------
from xml.sax import make_parser, handler, SAXParseException
#------------------------------------------------------------------------
#
# cnv2color
#
#------------------------------------------------------------------------
[docs]
def cnv2color(text):
"""
converts a hex value in the form of #XXXXXX into a tuple of integers
representing the RGB values
"""
return (int(text[1:3], 16), int(text[3:5], 16), int(text[5:7], 16))
#------------------------------------------------------------------------
#
# StyleSheetList
#
#------------------------------------------------------------------------
[docs]
class StyleSheetList:
"""
Interface into the user's defined style sheets. Each StyleSheetList
has a predefined default style specified by the report. Additional
styles are loaded from a specified XML file if it exists.
"""
def __init__(self, filename, defstyle):
"""
Create a new StyleSheetList from the specified default style and
any other styles that may be defined in the specified file.
file - XML file that contains style definitions
defstyle - default style
"""
defstyle.set_name('default')
self.map = { "default" : defstyle }
self.__file = filename
self.parse()
[docs]
def delete_style_sheet(self, name):
"""
Remove a style from the list. Since each style must have a
unique name, the name is used to delete the stylesheet.
name - Name of the style to delete
"""
del self.map[name]
[docs]
def get_style_sheet_map(self):
"""
Return the map of names to styles.
"""
return self.map
[docs]
def get_style_sheet(self, name):
"""
Return the StyleSheet associated with the name
name - name associated with the desired StyleSheet.
"""
return self.map[name]
[docs]
def get_style_names(self):
"Return a list of all the style names in the StyleSheetList"
return list(self.map.keys())
[docs]
def set_style_sheet(self, name, style):
"""
Add or replaces a StyleSheet in the StyleSheetList. The
default style may not be replaced.
name - name associated with the StyleSheet to add or replace.
style - definition of the StyleSheet
"""
style.set_name(name)
if name != "default":
self.map[name] = style
[docs]
def save(self):
"""
Saves the current StyleSheet definitions to the associated file.
"""
with open(self.__file, "w") as xml_file:
xml_file.write('<?xml version="1.0" encoding="utf-8"?>\n')
xml_file.write('<stylelist>\n')
for name in sorted(self.map.keys()): # enable diff of archived ones
if name == "default":
continue
sheet = self.map[name]
xml_file.write(' <sheet name="%s">\n' % escxml(name))
for p_name in sorted(sheet.get_paragraph_style_names()):
self.write_paragraph_style(xml_file, sheet, p_name)
for t_name in sorted(sheet.get_table_style_names()):
self.write_table_style(xml_file, sheet, t_name)
for c_name in sorted(sheet.get_cell_style_names()):
self.write_cell_style(xml_file, sheet, c_name)
for g_name in sorted(sheet.get_draw_style_names()):
self.write_graphics_style(xml_file, sheet, g_name)
xml_file.write(' </sheet>\n')
xml_file.write('</stylelist>\n')
[docs]
def write_paragraph_style(self, xml_file, sheet, p_name):
para = sheet.get_paragraph_style(p_name)
# Get variables for substitutions
font = para.get_font()
rmargin = float(para.get_right_margin())
lmargin = float(para.get_left_margin())
findent = float(para.get_first_indent())
tmargin = float(para.get_top_margin())
bmargin = float(para.get_bottom_margin())
padding = float(para.get_padding())
bg_color = para.get_background_color()
# Write out style definition
xml_file.write(
' <style name="%s">\n' % escxml(p_name) +
' <font ' +
'face="%d" ' % font.get_type_face() +
'size="%d" ' % font.get_size() +
'italic="%d" ' % font.get_italic() +
'bold="%d" ' % font.get_bold() +
'underline="%d" ' % font.get_underline() +
'color="#%02x%02x%02x" ' % font.get_color() +
'/>\n' +
' <para ' +
'description="%s" ' % escxml(para.get_description()) +
'rmargin="%.3f" ' % rmargin +
'lmargin="%.3f" ' % lmargin +
'first="%.3f" ' % findent +
'tmargin="%.3f" ' % tmargin +
'bmargin="%.3f" ' % bmargin +
'pad="%.3f" ' % padding +
'bgcolor="#%02x%02x%02x" ' % bg_color +
'level="%d" ' % para.get_header_level() +
'align="%d" ' % para.get_alignment() +
'tborder="%d" ' % para.get_top_border() +
'lborder="%d" ' % para.get_left_border() +
'rborder="%d" ' % para.get_right_border() +
'bborder="%d" ' % para.get_bottom_border() +
'/>\n' +
' </style>\n'
)
[docs]
def write_table_style(self, xml_file, sheet, t_name):
t_style = sheet.get_table_style(t_name)
# Write out style definition
xml_file.write(
' <style name="%s">\n' % escxml(t_name) +
' <table ' +
'description="%s" ' % escxml(t_style.get_description()) +
'width="%d" ' % t_style.get_width() +
'columns="%d"' % t_style.get_columns() +
'>\n')
for col in range(t_style.get_columns()):
column_width = t_style.get_column_width(col)
xml_file.write(' <column width="%d" />\n' % column_width)
xml_file.write(' </table>\n')
xml_file.write(' </style>\n')
[docs]
def write_cell_style(self, xml_file, sheet, c_name):
cell = sheet.get_cell_style(c_name)
# Write out style definition
xml_file.write(
' <style name="%s">\n' % escxml(c_name) +
' <cell ' +
'description="%s" ' % escxml(cell.get_description()) +
'lborder="%d" ' % cell.get_left_border() +
'rborder="%d" ' % cell.get_right_border() +
'tborder="%d" ' % cell.get_top_border() +
'bborder="%d" ' % cell.get_bottom_border() +
'pad="%.3f" ' % cell.get_padding() +
'/>\n' +
' </style>\n'
)
[docs]
def write_graphics_style(self, xml_file, sheet, g_name):
draw = sheet.get_draw_style(g_name)
# Write out style definition
xml_file.write(
' <style name="%s">\n' % escxml(g_name) +
' <draw ' +
'description="%s" ' % escxml(draw.get_description()) +
'para="%s" ' % draw.get_paragraph_style() +
'width="%.3f" ' % draw.get_line_width() +
'style="%d" ' % draw.get_line_style() +
'color="#%02x%02x%02x" ' % draw.get_color() +
'fillcolor="#%02x%02x%02x" ' % draw.get_fill_color() +
'shadow="%d" ' % draw.get_shadow() +
'space="%.3f" ' % draw.get_shadow_space() +
'/>\n' +
' </style>\n'
)
[docs]
def parse(self):
"""
Loads the StyleSheets from the associated file, if it exists.
"""
try:
if os.path.isfile(self.__file):
parser = make_parser()
parser.setContentHandler(SheetParser(self))
with open(self.__file) as the_file:
parser.parse(the_file)
except (IOError, OSError, SAXParseException):
pass
#------------------------------------------------------------------------
#
# StyleSheet
#
#------------------------------------------------------------------------
[docs]
class StyleSheet:
"""
A collection of named paragraph styles.
"""
def __init__(self, obj=None):
"""
Create a new empty StyleSheet.
:param obj: if not None, creates the StyleSheet from the values in
obj, instead of creating an empty StyleSheet
"""
self.para_styles = {}
self.draw_styles = {}
self.table_styles = {}
self.cell_styles = {}
self.name = ""
if obj is not None:
for style_name, style in obj.para_styles.items():
self.para_styles[style_name] = ParagraphStyle(style)
for style_name, style in obj.draw_styles.items():
self.draw_styles[style_name] = GraphicsStyle(style)
for style_name, style in obj.table_styles.items():
self.table_styles[style_name] = TableStyle(style)
for style_name, style in obj.cell_styles.items():
self.cell_styles[style_name] = TableCellStyle(style)
[docs]
def set_name(self, name):
"""
Set the name of the StyleSheet
:param name: The name to be given to the StyleSheet
"""
self.name = name
[docs]
def get_name(self):
"""
Return the name of the StyleSheet
"""
return self.name
[docs]
def clear(self):
"Remove all styles from the StyleSheet"
self.para_styles = {}
self.draw_styles = {}
self.table_styles = {}
self.cell_styles = {}
[docs]
def is_empty(self):
"Checks if any styles are defined"
style_count = len(self.para_styles) + \
len(self.draw_styles) + \
len(self.table_styles) + \
len(self.cell_styles)
if style_count > 0:
return False
else:
return True
[docs]
def add_paragraph_style(self, name, style):
"""
Add a paragraph style to the style sheet.
:param name: The name of the :class:`.ParagraphStyle`
:param style: :class:`.ParagraphStyle` instance to be added.
"""
self.para_styles[name] = ParagraphStyle(style)
[docs]
def get_paragraph_style(self, name):
"""
Return the :class:`.ParagraphStyle` associated with the name
:param name: name of the :class:`.ParagraphStyle` that is wanted
"""
return ParagraphStyle(self.para_styles[name])
[docs]
def get_paragraph_style_names(self):
"Return the list of paragraph names in the StyleSheet"
return list(self.para_styles.keys())
[docs]
def add_draw_style(self, name, style):
"""
Add a draw style to the style sheet.
:param name: The name of the :class:`.GraphicsStyle`
:param style: :class:`.GraphicsStyle` instance to be added.
"""
self.draw_styles[name] = GraphicsStyle(style)
[docs]
def get_draw_style(self, name):
"""
Return the :class:`.GraphicsStyle` associated with the name
:param name: name of the :class:`.GraphicsStyle` that is wanted
"""
return GraphicsStyle(self.draw_styles[name])
[docs]
def get_draw_style_names(self):
"Return the list of draw style names in the StyleSheet"
return list(self.draw_styles.keys())
[docs]
def add_table_style(self, name, style):
"""
Add a table style to the style sheet.
:param name: The name of the :class:`.TableStyle`
:param style: :class:`.TableStyle` instance to be added.
"""
self.table_styles[name] = TableStyle(style)
[docs]
def get_table_style(self, name):
"""
Return the :class:`.TableStyle` associated with the name
:param name: name of the :class:`.TableStyle` that is wanted
"""
return TableStyle(self.table_styles[name])
[docs]
def get_table_style_names(self):
"Return the list of table style names in the StyleSheet"
return list(self.table_styles.keys())
[docs]
def add_cell_style(self, name, style):
"""
Add a cell style to the style sheet.
:param name: The name of the :class:`.TableCellStyle`
:param style: :class:`.TableCellStyle` instance to be added.
"""
self.cell_styles[name] = TableCellStyle(style)
[docs]
def get_cell_style(self, name):
"""
Return the :class:`.TableCellStyle` associated with the name
:param name: name of the :class:`.TableCellStyle` that is wanted
"""
return TableCellStyle(self.cell_styles[name])
[docs]
def get_cell_style_names(self):
"Return the list of cell style names in the StyleSheet"
return list(self.cell_styles.keys())
#-------------------------------------------------------------------------
#
# SheetParser
#
#-------------------------------------------------------------------------
[docs]
class SheetParser(handler.ContentHandler):
"""
SAX parsing class for the StyleSheetList XML file.
"""
def __init__(self, sheetlist):
"""
Create a SheetParser class that populates the passed StyleSheetList
class.
sheetlist - :class:`StyleSheetList` instance to be loaded from the file.
"""
handler.ContentHandler.__init__(self)
self.sheetlist = sheetlist
self.f = None
self.p = None
self.s = None
self.t = None
self.columns_widths = []
self.sheet_name = None
self.style_name = None
[docs]
def startElement(self, tag, attrs):
"""
Overridden class that handles the start of a XML element
"""
if tag == "sheet":
self.s = StyleSheet(self.sheetlist.map["default"])
self.sheet_name = attrs['name']
elif tag == "font":
self.f = FontStyle()
self.f.set_type_face(int(attrs['face']))
self.f.set_size(int(attrs['size']))
self.f.set_italic(int(attrs['italic']))
self.f.set_bold(int(attrs['bold']))
self.f.set_underline(int(attrs['underline']))
self.f.set_color(cnv2color(attrs['color']))
elif tag == "para":
self.p = ParagraphStyle()
if 'description' in attrs:
self.p.set_description(attrs['description'])
self.p.set_right_margin(float(attrs['rmargin']))
self.p.set_left_margin(float(attrs['lmargin']))
self.p.set_first_indent(float(attrs['first']))
try:
# This is needed to read older style files
# lacking tmargin and bmargin
self.p.set_top_margin(float(attrs['tmargin']))
self.p.set_bottom_margin(float(attrs['bmargin']))
except KeyError:
pass
self.p.set_padding(float(attrs['pad']))
self.p.set_alignment(int(attrs['align']))
self.p.set_right_border(int(attrs['rborder']))
self.p.set_header_level(int(attrs['level']))
self.p.set_left_border(int(attrs['lborder']))
self.p.set_top_border(int(attrs['tborder']))
self.p.set_bottom_border(int(attrs['bborder']))
self.p.set_background_color(cnv2color(attrs['bgcolor']))
self.p.set_font(self.f)
elif tag == "style":
self.style_name = attrs['name']
elif tag == "table":
self.t = TableStyle()
if 'description' in attrs:
self.t.set_description(attrs['description'])
self.t.set_width(int(attrs['width']))
self.t.set_columns(int(attrs['columns']))
self.column_widths = []
elif tag == "column":
self.column_widths.append(int(attrs['width']))
elif tag == "cell":
self.c = TableCellStyle()
if 'description' in attrs:
self.c.set_description(attrs['description'])
self.c.set_left_border(int(attrs['lborder']))
self.c.set_right_border(int(attrs['rborder']))
self.c.set_top_border(int(attrs['tborder']))
self.c.set_bottom_border(int(attrs['bborder']))
self.c.set_padding(float(attrs['pad']))
elif tag == "draw":
self.g = GraphicsStyle()
if 'description' in attrs:
self.g.set_description(attrs['description'])
self.g.set_paragraph_style(attrs['para'])
self.g.set_line_width(float(attrs['width']))
self.g.set_line_style(int(attrs['style']))
self.g.set_color(cnv2color(attrs['color']))
self.g.set_fill_color(cnv2color(attrs['fillcolor']))
self.g.set_shadow(int(attrs['shadow']),
float(attrs['space']))
[docs]
def endElement(self, tag):
"Overridden class that handles the end of a XML element"
if tag == "sheet":
self.sheetlist.set_style_sheet(self.sheet_name, self.s)
elif tag == "para":
self.s.add_paragraph_style(self.style_name, self.p)
elif tag == "table":
self.t.set_column_widths(self.column_widths)
self.s.add_table_style(self.style_name, self.t)
elif tag == "cell":
self.s.add_cell_style(self.style_name, self.c)
elif tag == "draw":
self.s.add_draw_style(self.style_name, self.g)