e53c2ae8ad
svn: r11914
1432 lines
49 KiB
Python
1432 lines
49 KiB
Python
#
|
|
# Gramps - a GTK+/GNOME based genealogy program
|
|
#
|
|
# Copyright (C) 2007 Zsolt Foldvari
|
|
# Copyright (C) 2009 Benny Malengier
|
|
#
|
|
# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
#
|
|
|
|
# $Id$
|
|
|
|
"""Report output generator based on Cairo.
|
|
"""
|
|
|
|
#------------------------------------------------------------------------
|
|
#
|
|
# Python modules
|
|
#
|
|
#------------------------------------------------------------------------
|
|
from math import radians
|
|
from xml.sax.saxutils import escape
|
|
|
|
#------------------------------------------------------------------------
|
|
#
|
|
# Gramps modules
|
|
#
|
|
#------------------------------------------------------------------------
|
|
import BaseDoc
|
|
from ReportBase import ReportUtils
|
|
|
|
#------------------------------------------------------------------------
|
|
#
|
|
# Set up logging
|
|
#
|
|
#------------------------------------------------------------------------
|
|
import logging
|
|
log = logging.getLogger(".CairoDoc")
|
|
|
|
#-------------------------------------------------------------------------
|
|
#
|
|
# GTK modules
|
|
#
|
|
#-------------------------------------------------------------------------
|
|
import gtk
|
|
import pango
|
|
import pangocairo
|
|
|
|
#------------------------------------------------------------------------
|
|
#
|
|
# Constants
|
|
#
|
|
#------------------------------------------------------------------------
|
|
|
|
# each element draws some extra information useful for debugging
|
|
DEBUG = False
|
|
|
|
#------------------------------------------------------------------------
|
|
#
|
|
# Font selection
|
|
#
|
|
#------------------------------------------------------------------------
|
|
|
|
_TTF_FREEFONT = {
|
|
BaseDoc.FONT_SERIF: 'FreeSerif',
|
|
BaseDoc.FONT_SANS_SERIF: 'FreeSans',
|
|
BaseDoc.FONT_MONOSPACE: 'FreeMono',
|
|
}
|
|
|
|
_MS_TTFONT = {
|
|
BaseDoc.FONT_SERIF: 'Times New Roman',
|
|
BaseDoc.FONT_SANS_SERIF: 'Arial',
|
|
BaseDoc.FONT_MONOSPACE: 'Courier New',
|
|
}
|
|
|
|
_GNOME_FONT = {
|
|
BaseDoc.FONT_SERIF: 'Serif',
|
|
BaseDoc.FONT_SANS_SERIF: 'Sans',
|
|
BaseDoc.FONT_MONOSPACE: 'Monospace',
|
|
}
|
|
|
|
font_families = _GNOME_FONT
|
|
|
|
# FIXME debug logging does not work here.
|
|
def set_font_families():
|
|
##def set_font_families(pango_context):
|
|
"""Set the used font families depending on availability.
|
|
"""
|
|
global font_families
|
|
|
|
##families = pango_context.list_families()
|
|
families = pangocairo.cairo_font_map_get_default().list_families()
|
|
family_names = [family.get_name() for family in families]
|
|
|
|
fam = [f for f in _TTF_FREEFONT.values() if f in family_names]
|
|
if len(fam) == len(_TTF_FREEFONT):
|
|
font_families = _TTF_FREEFONT
|
|
log.debug('Using FreeFonts: %s' % font_families)
|
|
return
|
|
|
|
fam = [f for f in _MS_TTFONT.values() if f in family_names]
|
|
if len(fam) == len(_MS_TTFONT):
|
|
font_families = _MS_TTFONT
|
|
log.debug('Using MS TrueType fonts: %s' % font_families)
|
|
return
|
|
|
|
fam = [f for f in _GNOME_FONT.values() if f in family_names]
|
|
if len(fam) == len(_GNOME_FONT):
|
|
font_families = _GNOME_FONT
|
|
log.debug('Using Gnome fonts: %s' % font_families)
|
|
return
|
|
|
|
log.debug('No fonts found.')
|
|
|
|
set_font_families()
|
|
|
|
#------------------------------------------------------------------------
|
|
#
|
|
# Converter functions
|
|
#
|
|
#------------------------------------------------------------------------
|
|
|
|
def fontstyle_to_fontdescription(font_style):
|
|
"""Convert a BaseDoc.FontStyle instance to a pango.FontDescription one.
|
|
|
|
Font color and underline are not implemented in pango.FontDescription,
|
|
and have to be set with pango.Layout.set_attributes(attrlist) method.
|
|
|
|
"""
|
|
if font_style.get_bold():
|
|
f_weight = pango.WEIGHT_BOLD
|
|
else:
|
|
f_weight = pango.WEIGHT_NORMAL
|
|
|
|
if font_style.get_italic():
|
|
f_style = pango.STYLE_ITALIC
|
|
else:
|
|
f_style = pango.STYLE_NORMAL
|
|
|
|
font_description = pango.FontDescription(font_families[font_style.face])
|
|
font_description.set_size(int(round(font_style.get_size() * pango.SCALE)))
|
|
font_description.set_weight(f_weight)
|
|
font_description.set_style(f_style)
|
|
|
|
return font_description
|
|
|
|
def tabstops_to_tabarray(tab_stops, dpi):
|
|
"""Convert a list of tabs given in cm to a pango.TabArray.
|
|
"""
|
|
tab_array = pango.TabArray(len(tab_stops), False)
|
|
|
|
for index in range(len(tab_stops)):
|
|
location = tab_stops[index] * dpi * pango.SCALE / 2.54
|
|
tab_array.set_tab(index, pango.TAB_LEFT, int(location))
|
|
|
|
return tab_array
|
|
|
|
###------------------------------------------------------------------------
|
|
###
|
|
### Table row style
|
|
###
|
|
###------------------------------------------------------------------------
|
|
|
|
##class RowStyle(list):
|
|
##"""Specifies the format of a table row.
|
|
|
|
##RowStyle extents the available styles in BaseDoc.
|
|
|
|
##The RowStyle contains the width of each column as a percentage of the
|
|
##width of the full row. Note! The width of the row is not known until
|
|
##divide() or draw() method is called.
|
|
|
|
##"""
|
|
##def __init__(self):
|
|
##self.columns = []
|
|
|
|
##def set_columns(self, columns):
|
|
##"""Set the number of columns.
|
|
|
|
##@param columns: number of columns that should be used.
|
|
##@param type: int
|
|
|
|
##"""
|
|
##self.columns = columns
|
|
|
|
##def get_columns(self):
|
|
##"""Return the number of columns.
|
|
##"""
|
|
##return self.columns
|
|
|
|
##def set_column_widths(self, clist):
|
|
##"""Set the width of all the columns at once.
|
|
|
|
##@param clist: list of width of columns in % of the full row.
|
|
##@param tyle: list
|
|
|
|
##"""
|
|
##self.columns = len(clist)
|
|
##for i in range(self.columns):
|
|
##self.colwid[i] = clist[i]
|
|
|
|
##def set_column_width(self, index, width):
|
|
##"""
|
|
##Set the width of a specified column to the specified width.
|
|
|
|
##@param index: column being set (index starts at 0)
|
|
##@param width: percentage of the table width assigned to the column
|
|
##"""
|
|
##self.colwid[index] = width
|
|
|
|
##def get_column_width(self, index):
|
|
##"""
|
|
##Return the column width of the specified column as a percentage of
|
|
##the entire table width.
|
|
|
|
##@param index: column to return (index starts at 0)
|
|
##"""
|
|
##return self.colwid[index]
|
|
|
|
class FrameStyle(object):
|
|
"""Define the style properties of a Frame.
|
|
|
|
- width: Width of the frame in cm.
|
|
- height: Height of the frame in cm.
|
|
- align: Horizontal position to entire page.
|
|
Available values: 'left','center', 'right'.
|
|
- spacing: Tuple of spacing around the frame in cm. Order of values:
|
|
(left, right, top, bottom).
|
|
|
|
"""
|
|
def __init__(self, width=0, height=0, align='left', spacing=(0, 0, 0, 0)):
|
|
self.width = width
|
|
self.height = height
|
|
self.align = align
|
|
self.spacing = spacing
|
|
|
|
#------------------------------------------------------------------------
|
|
#
|
|
# Document element classes
|
|
#
|
|
#------------------------------------------------------------------------
|
|
|
|
class GtkDocBaseElement(object):
|
|
"""Base of all document elements.
|
|
|
|
Support document element structuring and can render itself onto
|
|
a Cairo surface.
|
|
|
|
There are two cathegories of methods:
|
|
1. hierarchy building methods (add_child, get_children, set_parent,
|
|
get_parent);
|
|
2. rendering methods (divide, draw).
|
|
|
|
The hierarchy building methods generally don't have to be overriden in
|
|
the subclass, while the rendering methods (divide, draw) must be
|
|
implemented in the subclasses.
|
|
|
|
"""
|
|
_type = 'BASE'
|
|
_allowed_children = []
|
|
|
|
def __init__(self, style=None):
|
|
self._parent = None
|
|
self._children = []
|
|
self._style = style
|
|
|
|
def get_type(self):
|
|
"""Get the type of this element.
|
|
"""
|
|
return self._type
|
|
|
|
def set_parent(self, parent):
|
|
"""Set the parent element of this element.
|
|
"""
|
|
self._parent = parent
|
|
|
|
def get_parent(self):
|
|
"""Get the parent element of this element.
|
|
"""
|
|
return self._parent
|
|
|
|
def add_child(self, element):
|
|
"""Add a child element.
|
|
|
|
Returns False if the child cannot be added (e.g. not an allowed type),
|
|
or True otherwise.
|
|
|
|
"""
|
|
# check if it is an allowed child for this type
|
|
if element.get_type() not in self._allowed_children:
|
|
log.debug("%r is not an allowed child for %r" %
|
|
(element.__class__, self.__class__))
|
|
return False
|
|
|
|
# append the child and set it's parent
|
|
self._children.append(element)
|
|
element.set_parent(self)
|
|
return True
|
|
|
|
def get_children(self):
|
|
"""Get the list of children of this element.
|
|
"""
|
|
return self._children
|
|
|
|
def divide(self, layout, width, height, dpi_x, dpi_y):
|
|
"""Divide the element into two depending on available space.
|
|
|
|
@param layout: pango layout to write on
|
|
@param type: pango.Layout
|
|
@param width: width of available space for this element
|
|
@param type: device points
|
|
@param height: height of available space for this element
|
|
@param type: device points
|
|
@param dpi_x: the horizontal resolution
|
|
@param type: dots per inch
|
|
@param dpi_y: the vertical resolution
|
|
@param type: dots per inch
|
|
|
|
@return: the divided element, and the height of the first part
|
|
@rtype: (GtkDocXXX-1, GtkDocXXX-2), device points
|
|
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
def draw(self, cairo_context, pango_layout, width, dpi_x, dpi_y):
|
|
"""Draw itself onto a cairo surface.
|
|
|
|
@param cairo_context: context to draw on
|
|
@param type: cairo.Context class
|
|
@param pango_layout: pango layout to write on
|
|
@param type: pango.Layout class
|
|
@param width: width of available space for this element
|
|
@param type: device points
|
|
@param dpi_x: the horizontal resolution
|
|
@param type: dots per inch
|
|
@param dpi_y: the vertical resolution
|
|
@param type: dots per inch
|
|
|
|
@return: height of the element
|
|
@rtype: device points
|
|
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
class GtkDocDocument(GtkDocBaseElement):
|
|
"""The whole document or a page.
|
|
"""
|
|
_type = 'DOCUMENT'
|
|
_allowed_children = ['PARAGRAPH', 'PAGEBREAK', 'TABLE', 'IMAGE', 'FRAME']
|
|
|
|
def draw(self, cairo_context, pango_layout, width, dpi_x, dpi_y):
|
|
|
|
x = y = elem_height = 0
|
|
|
|
for elem in self._children:
|
|
cairo_context.translate(x, elem_height)
|
|
elem_height = elem.draw(cairo_context, pango_layout,
|
|
width, dpi_x, dpi_y)
|
|
y += elem_height
|
|
|
|
return y
|
|
|
|
class GtkDocPagebreak(GtkDocBaseElement):
|
|
"""Implement a page break.
|
|
"""
|
|
_type = 'PAGEBREAK'
|
|
_allowed_children = []
|
|
|
|
def divide(self, layout, width, height, dpi_x, dpi_y):
|
|
return (None, None), 0
|
|
|
|
class GtkDocParagraph(GtkDocBaseElement):
|
|
"""Paragraph.
|
|
"""
|
|
_type = 'PARAGRAPH'
|
|
_allowed_children = []
|
|
|
|
# line spacing is not defined in BaseDoc.ParagraphStyle
|
|
spacing = 2
|
|
|
|
def __init__(self, style, leader=None):
|
|
GtkDocBaseElement.__init__(self, style)
|
|
|
|
if leader:
|
|
self._text = leader + '\t'
|
|
# FIXME append new tab to the existing tab list
|
|
self._style.set_tabs([-1 * self._style.get_first_indent()])
|
|
else:
|
|
self._text = ''
|
|
|
|
def add_text(self, text):
|
|
self._text = self._text + text
|
|
|
|
def divide(self, layout, width, height, dpi_x, dpi_y):
|
|
l_margin = self._style.get_left_margin() * dpi_x / 2.54
|
|
r_margin = self._style.get_right_margin() * dpi_x / 2.54
|
|
t_margin = self._style.get_top_margin() * dpi_y / 2.54
|
|
b_margin = self._style.get_bottom_margin() * dpi_y / 2.54
|
|
h_padding = self._style.get_padding() * dpi_x / 2.54
|
|
v_padding = self._style.get_padding() * dpi_y / 2.54
|
|
f_indent = self._style.get_first_indent() * dpi_x / 2.54
|
|
|
|
# calculate real width available for text
|
|
text_width = width - l_margin - 2 * h_padding - r_margin
|
|
if f_indent < 0:
|
|
text_width -= f_indent
|
|
layout.set_width(int(text_width * pango.SCALE))
|
|
|
|
# set paragraph properties
|
|
layout.set_wrap(pango.WRAP_WORD_CHAR)
|
|
layout.set_spacing(self.spacing * pango.SCALE)
|
|
layout.set_indent(int(f_indent * pango.SCALE))
|
|
layout.set_tabs(tabstops_to_tabarray(self._style.get_tabs(), dpi_x))
|
|
#
|
|
align = self._style.get_alignment_text()
|
|
if align == 'left':
|
|
layout.set_alignment(pango.ALIGN_LEFT)
|
|
elif align == 'right':
|
|
layout.set_alignment(pango.ALIGN_RIGHT)
|
|
elif align == 'center':
|
|
layout.set_alignment(pango.ALIGN_CENTER)
|
|
elif align == 'justify':
|
|
layout.set_justify(True)
|
|
else:
|
|
raise ValueError
|
|
#
|
|
font_style = self._style.get_font()
|
|
layout.set_font_description(fontstyle_to_fontdescription(font_style))
|
|
|
|
# calculate the height of one line
|
|
layout.set_text('Test')
|
|
layout_width, layout_height = layout.get_pixel_size()
|
|
line_height = layout_height + self.spacing
|
|
# and the number of lines fit on the available height
|
|
text_height = height - t_margin - 2 * v_padding
|
|
line_per_height = text_height / line_height
|
|
|
|
# if nothing fits
|
|
if line_per_height < 1:
|
|
return (None, self), 0
|
|
|
|
# calculate where to cut the paragraph
|
|
layout.set_markup(self._text)
|
|
layout_width, layout_height = layout.get_pixel_size()
|
|
line_count = layout.get_line_count()
|
|
|
|
# if all paragraph fits we don't need to cut
|
|
if line_count <= line_per_height:
|
|
paragraph_height = (layout_height + t_margin + (2 * v_padding))
|
|
if height - paragraph_height > b_margin:
|
|
paragraph_height += b_margin
|
|
return (self, None), paragraph_height
|
|
|
|
# get index of first character which doesn't fit on available height
|
|
layout_line = layout.get_line(int(line_per_height))
|
|
index = layout_line.start_index
|
|
# and divide the text, first create the second part
|
|
new_style = BaseDoc.ParagraphStyle(self._style)
|
|
new_style.set_top_margin(0)
|
|
new_paragraph = GtkDocParagraph(new_style)
|
|
new_paragraph.add_text(self._text.encode('utf-8')[index:])
|
|
# then update the first one
|
|
self._text = self._text.encode('utf-8')[:index]
|
|
self._style.set_bottom_margin(0)
|
|
|
|
# FIXME do we need to return the proper height???
|
|
#paragraph_height = line_height * line_count + t_margin + 2 * v_padding
|
|
paragraph_height = 0
|
|
return (self, new_paragraph), paragraph_height
|
|
|
|
def draw(self, cr, layout, width, dpi_x, dpi_y):
|
|
l_margin = self._style.get_left_margin() * dpi_x / 2.54
|
|
r_margin = self._style.get_right_margin() * dpi_x / 2.54
|
|
t_margin = self._style.get_top_margin() * dpi_y / 2.54
|
|
b_margin = self._style.get_bottom_margin() * dpi_y / 2.54
|
|
h_padding = self._style.get_padding() * dpi_x / 2.54
|
|
v_padding = self._style.get_padding() * dpi_y / 2.54
|
|
f_indent = self._style.get_first_indent() * dpi_x / 2.54
|
|
|
|
# calculate real width available for text
|
|
text_width = width - l_margin - 2 * h_padding - r_margin
|
|
if f_indent < 0:
|
|
text_width -= f_indent
|
|
layout.set_width(int(text_width * pango.SCALE))
|
|
|
|
# set paragraph properties
|
|
layout.set_wrap(pango.WRAP_WORD_CHAR)
|
|
layout.set_spacing(self.spacing * pango.SCALE)
|
|
layout.set_indent(int(f_indent * pango.SCALE))
|
|
layout.set_tabs(tabstops_to_tabarray(self._style.get_tabs(), dpi_x))
|
|
#
|
|
align = self._style.get_alignment_text()
|
|
if align == 'left':
|
|
layout.set_alignment(pango.ALIGN_LEFT)
|
|
elif align == 'right':
|
|
layout.set_alignment(pango.ALIGN_RIGHT)
|
|
elif align == 'center':
|
|
layout.set_alignment(pango.ALIGN_CENTER)
|
|
elif align == 'justify':
|
|
layout.set_justify(True)
|
|
#
|
|
font_style = self._style.get_font()
|
|
layout.set_font_description(fontstyle_to_fontdescription(font_style))
|
|
|
|
# layout the text
|
|
layout.set_markup(self._text)
|
|
layout_width, layout_height = layout.get_pixel_size()
|
|
|
|
# render the layout onto the cairo surface
|
|
x = l_margin + h_padding
|
|
if f_indent < 0:
|
|
x += f_indent
|
|
cr.move_to(x, t_margin + v_padding)
|
|
cr.set_source_rgb(*ReportUtils.rgb_color(font_style.get_color()))
|
|
cr.show_layout(layout)
|
|
|
|
# calculate the full paragraph height
|
|
height = layout_height + t_margin + 2*v_padding + b_margin
|
|
|
|
# draw the borders
|
|
if self._style.get_top_border():
|
|
cr.move_to(l_margin, t_margin)
|
|
cr.rel_line_to(width - l_margin - r_margin, 0)
|
|
if self._style.get_right_border():
|
|
cr.move_to(width - r_margin, t_margin)
|
|
cr.rel_line_to(0, height - t_margin - b_margin)
|
|
if self._style.get_bottom_border():
|
|
cr.move_to(l_margin, height - b_margin)
|
|
cr.rel_line_to(width - l_margin - r_margin, 0)
|
|
if self._style.get_left_border():
|
|
cr.move_to(l_margin, t_margin)
|
|
cr.line_to(0, height - t_margin - b_margin)
|
|
|
|
cr.set_line_width(1)
|
|
cr.set_source_rgb(0, 0, 0)
|
|
cr.stroke()
|
|
|
|
if DEBUG:
|
|
cr.set_line_width(0.1)
|
|
cr.set_source_rgb(1.0, 0, 0)
|
|
cr.rectangle(0, 0, width, height)
|
|
cr.stroke()
|
|
cr.set_source_rgb(0, 0, 1.0)
|
|
cr.rectangle(l_margin, t_margin,
|
|
width-l_margin-r_margin, height-t_margin-b_margin)
|
|
cr.stroke()
|
|
|
|
return height
|
|
|
|
class GtkDocTable(GtkDocBaseElement):
|
|
"""Implement a table.
|
|
"""
|
|
_type = 'TABLE'
|
|
_allowed_children = ['ROW']
|
|
|
|
def divide(self, layout, width, height, dpi_x, dpi_y):
|
|
#calculate real table width
|
|
table_width = width * self._style.get_width() / 100
|
|
|
|
# calculate the height of each row
|
|
table_height = 0
|
|
row_index = 0
|
|
while row_index < len(self._children):
|
|
row = self._children[row_index]
|
|
(r1, r2), row_height = row.divide(layout, table_width, height,
|
|
dpi_x, dpi_y)
|
|
if table_height + row_height >= height:
|
|
break
|
|
table_height += row_height
|
|
row_index += 1
|
|
|
|
# divide the table if any row did not fit
|
|
new_table = None
|
|
if row_index < len(self._children):
|
|
new_table = GtkDocTable(self._style)
|
|
for row in self._children[row_index:]:
|
|
new_table.add_child(row)
|
|
del self._children[row_index:]
|
|
|
|
return (self, new_table), table_height
|
|
|
|
def draw(self, cr, layout, width, dpi_x, dpi_y):
|
|
#calculate real table width
|
|
table_width = width * self._style.get_width() / 100
|
|
# TODO is a table always left aligned??
|
|
table_height = 0
|
|
|
|
# draw all the rows
|
|
for row in self._children:
|
|
cr.save()
|
|
cr.translate(0, table_height)
|
|
row_height = row.draw(cr, layout, table_width, dpi_x, dpi_y)
|
|
cr.restore()
|
|
table_height += row_height
|
|
|
|
if DEBUG:
|
|
cr.set_line_width(0.1)
|
|
cr.set_source_rgb(1.0, 0, 0)
|
|
cr.rectangle(0, 0, table_width, table_height)
|
|
cr.stroke()
|
|
|
|
return table_height
|
|
|
|
class GtkDocTableRow(GtkDocBaseElement):
|
|
"""Implement a row in a table.
|
|
"""
|
|
_type = 'ROW'
|
|
_allowed_children = ['CELL']
|
|
|
|
def divide(self, layout, width, height, dpi_x, dpi_y):
|
|
# the highest cell gives the height of the row
|
|
cell_heights = []
|
|
cell_width_iter = self._style.__iter__()
|
|
for cell in self._children:
|
|
cell_width = 0
|
|
for i in range(cell.get_span()):
|
|
cell_width += cell_width_iter.next()
|
|
cell_width = cell_width * width / 100
|
|
(c1, c2), cell_height = cell.divide(layout, cell_width, height,
|
|
dpi_x, dpi_y)
|
|
cell_heights.append(cell_height)
|
|
|
|
# save height [inch] of the row to be able to draw exact cell border
|
|
row_height = max(cell_heights)
|
|
self.height = row_height / dpi_y
|
|
|
|
# a row can't be divided, return the height
|
|
return (self, None), row_height
|
|
|
|
def draw(self, cr, layout, width, dpi_x, dpi_y):
|
|
cr.save()
|
|
|
|
# get the height of this row
|
|
row_height = self.height * dpi_y
|
|
|
|
# draw all the cells in the row
|
|
cell_width_iter = self._style.__iter__()
|
|
for cell in self._children:
|
|
cell_width = 0
|
|
for i in range(cell.get_span()):
|
|
cell_width += cell_width_iter.next()
|
|
cell_width = cell_width * width / 100
|
|
cell.draw(cr, layout, cell_width, row_height, dpi_x, dpi_y)
|
|
cr.translate(cell_width, 0)
|
|
cr.restore()
|
|
|
|
if DEBUG:
|
|
cr.set_line_width(0.1)
|
|
cr.set_source_rgb(0, 0, 1.0)
|
|
cr.rectangle(0, 0, width, row_height)
|
|
cr.stroke()
|
|
|
|
return row_height
|
|
|
|
class GtkDocTableCell(GtkDocBaseElement):
|
|
"""Implement a cell in a table row.
|
|
"""
|
|
_type = 'CELL'
|
|
_allowed_children = ['PARAGRAPH', 'IMAGE']
|
|
|
|
def __init__(self, style, span=1):
|
|
GtkDocBaseElement.__init__(self, style)
|
|
self._span = span
|
|
|
|
def get_span(self):
|
|
return self._span
|
|
|
|
def divide(self, layout, width, height, dpi_x, dpi_y):
|
|
h_padding = self._style.get_padding() * dpi_x / 2.54
|
|
v_padding = self._style.get_padding() * dpi_y / 2.54
|
|
|
|
# calculate real available width
|
|
width -= 2 * h_padding
|
|
|
|
# calculate height of each children
|
|
cell_height = 0
|
|
for child in self._children:
|
|
(e1, e2), child_height = child.divide(layout, width, height,
|
|
dpi_x, dpi_y)
|
|
cell_height += child_height
|
|
|
|
# calculate real height
|
|
cell_height += 2 * v_padding
|
|
|
|
# a cell can't be divided, return the heigth
|
|
return (self, None), cell_height
|
|
|
|
def draw(self, cr, layout, width, cell_height, dpi_x, dpi_y):
|
|
"""Draw a cell.
|
|
|
|
This draw method is a bit different from the others, as common
|
|
cell height of all cells in a row is also given as parameter.
|
|
This is needed to be able to draw proper vertical borders around
|
|
each cell, i.e. the border should be as long as the highest cell
|
|
in the given row.
|
|
|
|
"""
|
|
h_padding = self._style.get_padding() * dpi_x / 2.54
|
|
v_padding = self._style.get_padding() * dpi_y / 2.54
|
|
|
|
# calculate real available width
|
|
i_width = width - 2 * h_padding
|
|
|
|
# draw children
|
|
cr.save()
|
|
cr.translate(h_padding, v_padding)
|
|
for child in self._children:
|
|
child_height = child.draw(cr, layout, i_width, dpi_x, dpi_y)
|
|
cr.translate(0, child_height)
|
|
cr.restore()
|
|
|
|
# draw the borders
|
|
if self._style.get_top_border():
|
|
cr.move_to(0, 0)
|
|
cr.rel_line_to(width , 0)
|
|
if self._style.get_right_border():
|
|
cr.move_to(width, 0)
|
|
cr.rel_line_to(0, cell_height)
|
|
if self._style.get_bottom_border():
|
|
cr.move_to(0, cell_height)
|
|
cr.rel_line_to(width, 0)
|
|
if self._style.get_left_border():
|
|
cr.move_to(0, 0)
|
|
cr.line_to(0, cell_height)
|
|
|
|
cr.set_line_width(1)
|
|
cr.set_source_rgb(0, 0, 0)
|
|
cr.stroke()
|
|
|
|
if DEBUG:
|
|
cr.set_line_width(0.1)
|
|
cr.set_source_rgb(0, 1.0, 0)
|
|
cr.rectangle(0, 0, width, cell_height)
|
|
cr.stroke()
|
|
|
|
return cell_height
|
|
|
|
class GtkDocPicture(GtkDocBaseElement):
|
|
"""Implement an image.
|
|
"""
|
|
_type = 'IMAGE'
|
|
_allowed_children = []
|
|
|
|
def __init__(self, style, filename, width, height):
|
|
GtkDocBaseElement.__init__(self, style)
|
|
self._filename = filename
|
|
self._width = width
|
|
self._height = height
|
|
|
|
def divide(self, layout, width, height, dpi_x, dpi_y):
|
|
img_width = self._width * dpi_x / 2.54
|
|
img_height = self._height * dpi_y / 2.54
|
|
|
|
# image can't be divided, a new page must begin
|
|
# if it can't fit on the current one
|
|
if img_height <= height:
|
|
return (self, None), img_height
|
|
else:
|
|
return (None, self), 0
|
|
|
|
def draw(self, cr, layout, width, dpi_x, dpi_y):
|
|
img_width = self._width * dpi_x / 2.54
|
|
img_height = self._height * dpi_y / 2.54
|
|
|
|
if self._style == 'right':
|
|
l_margin = width - img_width
|
|
elif self._style == 'center':
|
|
l_margin = (width - img_width) / 2.0
|
|
else:
|
|
l_margin = 0
|
|
|
|
# load the image and get its extents
|
|
pixbuf = gtk.gdk.pixbuf_new_from_file(self._filename)
|
|
pixbuf_width = pixbuf.get_width()
|
|
pixbuf_height = pixbuf.get_height()
|
|
|
|
# calculate the scale to fit image into the set extents
|
|
scale = min(img_width / pixbuf_width, img_height / pixbuf_height)
|
|
|
|
# draw the image
|
|
cr.save()
|
|
cr.translate(l_margin, 0)
|
|
cr.scale(scale, scale)
|
|
gcr = gtk.gdk.CairoContext(cr)
|
|
gcr.set_source_pixbuf(pixbuf,
|
|
(img_width / scale - pixbuf_width) / 2,
|
|
(img_height / scale - pixbuf_height) / 2)
|
|
cr.rectangle(0 , 0, img_width / scale, img_height / scale)
|
|
##gcr.set_source_pixbuf(pixbuf,
|
|
##(img_width - pixbuf_width) / 2,
|
|
##(img_height - pixbuf_height) / 2)
|
|
##cr.rectangle(0 , 0, img_width, img_height)
|
|
##cr.scale(scale, scale)
|
|
cr.fill()
|
|
cr.restore()
|
|
|
|
if DEBUG:
|
|
cr.set_line_width(0.1)
|
|
cr.set_source_rgb(1.0, 0, 0)
|
|
cr.rectangle(l_margin, 0, img_width, img_height)
|
|
cr.stroke()
|
|
|
|
return (img_height)
|
|
|
|
class GtkDocFrame(GtkDocBaseElement):
|
|
"""Implement a frame.
|
|
"""
|
|
_type = 'FRAME'
|
|
_allowed_children = ['LINE', 'POLYGON', 'BOX', 'TEXT']
|
|
|
|
def divide(self, layout, width, height, dpi_x, dpi_y):
|
|
frame_width = round(self._style.width * dpi_x / 2.54)
|
|
frame_height = round(self._style.height * dpi_y / 2.54)
|
|
t_margin = self._style.spacing[2] * dpi_y / 2.54
|
|
b_margin = self._style.spacing[3] * dpi_y / 2.54
|
|
|
|
# frame can't be divided, a new page must begin
|
|
# if it can't fit on the current one
|
|
if frame_height + t_margin + b_margin <= height:
|
|
return (self, None), frame_height + t_margin + b_margin
|
|
elif frame_height + t_margin <= height:
|
|
return (self, None), height
|
|
else:
|
|
return (None, self), 0
|
|
|
|
def draw(self, cr, layout, width, dpi_x, dpi_y):
|
|
frame_width = self._style.width * dpi_x / 2.54
|
|
frame_height = self._style.height * dpi_y / 2.54
|
|
l_margin = self._style.spacing[0] * dpi_y / 2.54
|
|
r_margin = self._style.spacing[1] * dpi_y / 2.54
|
|
t_margin = self._style.spacing[2] * dpi_y / 2.54
|
|
b_margin = self._style.spacing[3] * dpi_y / 2.54
|
|
|
|
if self._style.align == 'left':
|
|
x_offset = l_margin
|
|
elif self._style.align == 'right':
|
|
x_offset = width - r_margin - frame_width
|
|
elif self._style.align == 'center':
|
|
x_offset = (width - frame_width) / 2.0
|
|
else:
|
|
raise ValueError
|
|
|
|
# draw each element in the frame
|
|
cr.save()
|
|
cr.translate(x_offset, t_margin)
|
|
cr.rectangle(0, 0, frame_width, frame_height)
|
|
cr.clip()
|
|
|
|
for elem in self._children:
|
|
elem.draw(cr, layout, frame_width, dpi_x, dpi_y)
|
|
|
|
cr.restore()
|
|
|
|
if DEBUG:
|
|
cr.set_line_width(0.1)
|
|
cr.set_source_rgb(1.0, 0, 0)
|
|
cr.rectangle(x_offset, t_margin, frame_width, frame_height)
|
|
cr.stroke()
|
|
|
|
return frame_height + t_margin + b_margin
|
|
|
|
class GtkDocLine(GtkDocBaseElement):
|
|
"""Implement a line.
|
|
"""
|
|
_type = 'LINE'
|
|
_allowed_children = []
|
|
|
|
def __init__(self, style, x1, y1, x2, y2):
|
|
GtkDocBaseElement.__init__(self, style)
|
|
self._start = (x1, y1)
|
|
self._end = (x2, y2)
|
|
|
|
def draw(self, cr, layout, width, dpi_x, dpi_y):
|
|
start = (self._start[0] * dpi_x / 2.54, self._start[1] * dpi_y / 2.54)
|
|
end = (self._end[0] * dpi_x / 2.54, self._end[1] * dpi_y / 2.54)
|
|
line_color = ReportUtils.rgb_color(self._style.get_color())
|
|
|
|
cr.save()
|
|
cr.set_source_rgb(*line_color)
|
|
cr.set_line_width(self._style.get_line_width())
|
|
# TODO line style
|
|
cr.move_to(*start)
|
|
cr.line_to(*end)
|
|
cr.stroke()
|
|
cr.restore()
|
|
|
|
return 0
|
|
|
|
class GtkDocPolygon(GtkDocBaseElement):
|
|
"""Implement a line.
|
|
"""
|
|
_type = 'POLYGON'
|
|
_allowed_children = []
|
|
|
|
def __init__(self, style, path):
|
|
GtkDocBaseElement.__init__(self, style)
|
|
self._path = path
|
|
|
|
def draw(self, cr, layout, width, dpi_x, dpi_y):
|
|
path = [(x * dpi_x / 2.54, y * dpi_y / 2.54) for (x, y) in self._path]
|
|
path_start = path.pop(0)
|
|
path_stroke_color = ReportUtils.rgb_color(self._style.get_color())
|
|
path_fill_color = ReportUtils.rgb_color(self._style.get_fill_color())
|
|
|
|
cr.save()
|
|
cr.move_to(*path_start)
|
|
for (x, y) in path:
|
|
cr.line_to(x, y)
|
|
cr.close_path()
|
|
cr.set_source_rgb(*path_fill_color)
|
|
cr.fill_preserve()
|
|
cr.set_source_rgb(*path_stroke_color)
|
|
cr.set_line_width(self._style.get_line_width())
|
|
# TODO line style
|
|
cr.stroke()
|
|
cr.restore()
|
|
|
|
return 0
|
|
|
|
class GtkDocBox(GtkDocBaseElement):
|
|
"""Implement a box with optional shadow around it.
|
|
"""
|
|
_type = 'BOX'
|
|
_allowed_children = []
|
|
|
|
def __init__(self, style, x, y, width, height):
|
|
GtkDocBaseElement.__init__(self, style)
|
|
self._x = x
|
|
self._y = y
|
|
self._width = width
|
|
self._height = height
|
|
|
|
def draw(self, cr, layout, width, dpi_x, dpi_y):
|
|
box_x = self._x * dpi_x / 2.54
|
|
box_y = self._y * dpi_y / 2.54
|
|
box_width = self._width * dpi_x / 2.54
|
|
box_height = self._height * dpi_y / 2.54
|
|
|
|
box_stroke_color = ReportUtils.rgb_color((0, 0, 0))
|
|
box_fill_color = ReportUtils.rgb_color(self._style.get_fill_color())
|
|
shadow_color = ReportUtils.rgb_color((192, 192, 192))
|
|
|
|
cr.save()
|
|
|
|
cr.set_line_width(self._style.get_line_width())
|
|
# TODO line style
|
|
|
|
if self._style.get_shadow():
|
|
shadow_x = box_x + self._style.get_shadow_space() * dpi_x / 2.54
|
|
shadow_y = box_y + self._style.get_shadow_space() * dpi_y / 2.54
|
|
|
|
cr.set_source_rgb(*shadow_color)
|
|
cr.rectangle(shadow_x, shadow_y, box_width, box_height)
|
|
cr.fill()
|
|
|
|
cr.rectangle(box_x, box_y, box_width, box_height)
|
|
cr.set_source_rgb(*box_fill_color)
|
|
cr.fill_preserve()
|
|
cr.set_source_rgb(*box_stroke_color)
|
|
cr.stroke()
|
|
|
|
cr.restore()
|
|
|
|
return 0
|
|
|
|
class GtkDocText(GtkDocBaseElement):
|
|
"""Implement a text on graphical reports.
|
|
"""
|
|
_type = 'TEXT'
|
|
_allowed_children = []
|
|
|
|
# line spacing is not defined in BaseDoc.ParagraphStyle
|
|
spacing = 0
|
|
|
|
def __init__(self, style, vertical_alignment, text, x, y, angle=0):
|
|
GtkDocBaseElement.__init__(self, style)
|
|
self._align_y = vertical_alignment
|
|
self._text = text
|
|
self._x = x
|
|
self._y = y
|
|
self._angle = angle
|
|
|
|
def draw(self, cr, layout, width, dpi_x, dpi_y):
|
|
text_x = self._x * dpi_x / 2.54
|
|
text_y = self._y * dpi_y / 2.54
|
|
|
|
# turn off text wrapping
|
|
layout.set_width(-1)
|
|
|
|
# set paragraph properties
|
|
layout.set_spacing(self.spacing * pango.SCALE)
|
|
#
|
|
align = self._style.get_alignment_text()
|
|
if align == 'left':
|
|
layout.set_alignment(pango.ALIGN_LEFT)
|
|
elif align == 'right':
|
|
layout.set_alignment(pango.ALIGN_RIGHT)
|
|
elif align == 'center':
|
|
layout.set_alignment(pango.ALIGN_CENTER)
|
|
elif align == 'justify':
|
|
layout.set_justify(True)
|
|
else:
|
|
raise ValueError
|
|
#
|
|
font_style = self._style.get_font()
|
|
layout.set_font_description(fontstyle_to_fontdescription(font_style))
|
|
|
|
# layout the text
|
|
layout.set_markup(self._text)
|
|
layout_width, layout_height = layout.get_pixel_size()
|
|
|
|
# calculate horizontal and vertical alignment shift
|
|
if align == 'left':
|
|
align_x = 0
|
|
elif align == 'right':
|
|
align_x = - layout_width
|
|
elif align == 'center' or align == 'justify':
|
|
align_x = - layout_width / 2
|
|
else:
|
|
raise ValueError
|
|
|
|
if self._align_y == 'top':
|
|
align_y = 0
|
|
elif self._align_y == 'center':
|
|
align_y = - layout_height / 2
|
|
elif self._align_y == 'bottom':
|
|
align_y = - layout_height
|
|
else:
|
|
raise ValueError
|
|
|
|
# render the layout onto the cairo surface
|
|
cr.save()
|
|
cr.translate(text_x, text_y)
|
|
cr.rotate(radians(self._angle))
|
|
cr.move_to(align_x, align_y)
|
|
cr.set_source_rgb(*ReportUtils.rgb_color(font_style.get_color()))
|
|
cr.show_layout(layout)
|
|
cr.restore()
|
|
|
|
return layout_height
|
|
|
|
#------------------------------------------------------------------------
|
|
#
|
|
# CairoDoc class
|
|
#
|
|
#------------------------------------------------------------------------
|
|
class CairoDoc(BaseDoc.BaseDoc, BaseDoc.TextDoc, BaseDoc.DrawDoc):
|
|
"""Act as an abstract document that can render onto a cairo context.
|
|
|
|
Maintains an abstract model of the document. The root of this abstract
|
|
document is self._doc. The model is build via the subclassed BaseDoc, and
|
|
the implemented TextDoc, DrawDoc interface methods.
|
|
|
|
It can render the model onto cairo context pages, according to the received
|
|
page style.
|
|
|
|
"""
|
|
STYLETAG_TO_PROPERTY = {
|
|
BaseDoc.TextDoc.FONTCOLOR : 'foreground',
|
|
BaseDoc.TextDoc.HIGHLIGHT : 'background',
|
|
BaseDoc.TextDoc.FONTFACE : 'face',
|
|
BaseDoc.TextDoc.FONTSIZE : 'size',
|
|
}
|
|
|
|
# overwrite base class attributes
|
|
BaseDoc.TextDoc.SUPPORTED_MARKUP = [
|
|
BaseDoc.TextDoc.BOLD,
|
|
BaseDoc.TextDoc.ITALIC,
|
|
BaseDoc.TextDoc.UNDERLINE,
|
|
BaseDoc.TextDoc.FONTFACE,
|
|
BaseDoc.TextDoc.FONTSIZE,
|
|
BaseDoc.TextDoc.FONTCOLOR,
|
|
BaseDoc.TextDoc.HIGHLIGHT,
|
|
BaseDoc.TextDoc.SUPERSCRIPT ]
|
|
|
|
BaseDoc.TextDoc.STYLETAG_MARKUP = {
|
|
BaseDoc.TextDoc.BOLD : ("<b>", "</b>"),
|
|
BaseDoc.TextDoc.ITALIC : ("<i>", "</i>"),
|
|
BaseDoc.TextDoc.UNDERLINE : ("<u>", "</u>"),
|
|
BaseDoc.TextDoc.SUPERSCRIPT : ("<sup>", "</sup>"),
|
|
}
|
|
|
|
BaseDoc.TextDoc.ESCAPE_FUNC = lambda x: escape
|
|
|
|
# BaseDoc implementation
|
|
|
|
def open(self, filename):
|
|
self._filename = filename
|
|
self._doc = GtkDocDocument()
|
|
self._active_element = self._doc
|
|
self._pages = []
|
|
self._elements_to_paginate = []
|
|
|
|
def close(self):
|
|
self.run()
|
|
|
|
# TextDoc implementation
|
|
|
|
def page_break(self):
|
|
self._active_element.add_child(GtkDocPagebreak())
|
|
|
|
def start_bold(self):
|
|
self.__write_text('<b>', markup=True)
|
|
|
|
def end_bold(self):
|
|
self.__write_text('</b>', markup=True)
|
|
|
|
def start_superscript(self):
|
|
self.__write_text('<small><sup>', markup=True)
|
|
|
|
def end_superscript(self):
|
|
self.__write_text('</sup></small>', markup=True)
|
|
|
|
def start_paragraph(self, style_name, leader=None):
|
|
style_sheet = self.get_style_sheet()
|
|
style = style_sheet.get_paragraph_style(style_name)
|
|
|
|
new_paragraph = GtkDocParagraph(style, leader)
|
|
self._active_element.add_child(new_paragraph)
|
|
self._active_element = new_paragraph
|
|
|
|
def end_paragraph(self):
|
|
self._active_element = self._active_element.get_parent()
|
|
|
|
def start_table(self, name, style_name):
|
|
style_sheet = self.get_style_sheet()
|
|
style = style_sheet.get_table_style(style_name)
|
|
|
|
new_table = GtkDocTable(style)
|
|
self._active_element.add_child(new_table)
|
|
self._active_element = new_table
|
|
|
|
# we need to remember the column width list from the table style.
|
|
# this is an ugly hack, but got no better idea.
|
|
self._active_row_style = []
|
|
for i in range(style.get_columns()):
|
|
self._active_row_style.append(style.get_column_width(i))
|
|
|
|
def end_table(self):
|
|
self._active_element = self._active_element.get_parent()
|
|
|
|
def start_row(self):
|
|
new_row = GtkDocTableRow(self._active_row_style)
|
|
self._active_element.add_child(new_row)
|
|
self._active_element = new_row
|
|
|
|
def end_row(self):
|
|
self._active_element = self._active_element.get_parent()
|
|
|
|
def start_cell(self, style_name, span=1):
|
|
style_sheet = self.get_style_sheet()
|
|
style = style_sheet.get_cell_style(style_name)
|
|
|
|
new_cell = GtkDocTableCell(style, span)
|
|
self._active_element.add_child(new_cell)
|
|
self._active_element = new_cell
|
|
|
|
def end_cell(self):
|
|
self._active_element = self._active_element.get_parent()
|
|
|
|
def _create_xmltag(self, type, value):
|
|
"""
|
|
overwrites the method in BaseDoc.TextDoc.
|
|
creates the pango xml tags needed for non bool style types
|
|
"""
|
|
if type not in self.SUPPORTED_MARKUP:
|
|
return None
|
|
if type == BaseDoc.TextDoc.FONTSIZE:
|
|
#size is in thousandths of a point in pango
|
|
value = str(1000 * value)
|
|
|
|
return ('<span %s="%s">' % (self.STYLETAG_TO_PROPERTY[type], value),
|
|
'</span>')
|
|
|
|
def write_note(self, text, format, style_name):
|
|
"""
|
|
Method to write the note objects text on a
|
|
Document
|
|
"""
|
|
# We need to escape the text here for later pango.Layout.set_markup
|
|
# calls. This way we save the markup created by the report
|
|
# The markup in the note editor is not in the text so is not
|
|
# considered. It must be added by pango too
|
|
if format == 1:
|
|
#preformatted, retain whitespace. Cairo retains \n automatically,
|
|
#so use \n\n for paragraph detection
|
|
#this is bad code, a user can type spaces between paragraph!
|
|
for line in text.split('\n\n'):
|
|
self.start_paragraph(style_name)
|
|
self.write_text(line)
|
|
self.end_paragraph()
|
|
elif format == 0:
|
|
#this is bad code, a user can type spaces between paragraph!
|
|
for line in text.split('\n\n'):
|
|
self.start_paragraph(style_name)
|
|
line = line.replace('\n',' ')
|
|
line = ' '.join(line.split())
|
|
self.write_text(line)
|
|
self.end_paragraph()
|
|
|
|
def write_styled_note(self, styledtext, format, style_name):
|
|
"""
|
|
Convenience function to write a styledtext to the cairo doc.
|
|
styledtext : assumed a StyledText object to write
|
|
format : = 0 : Flowed, = 1 : Preformatted
|
|
style_name : name of the style to use for default presentation
|
|
|
|
@note: text=normal text, p_text=text with pango markup, s_tags=styled
|
|
text tags, p
|
|
"""
|
|
text = str(styledtext)
|
|
|
|
s_tags = styledtext.get_tags()
|
|
markuptext = self._add_markup_from_styled(text, s_tags)
|
|
|
|
if format == 1:
|
|
#preformatted, retain whitespace. Cairo retains \n automatically,
|
|
#so use \n\n for paragraph detection
|
|
#FIXME: following split should be regex to match \n\s*\n instead?
|
|
for line in markuptext.split('\n\n'):
|
|
self.start_paragraph(style_name)
|
|
self.__write_text(line, markup=True)
|
|
self.end_paragraph()
|
|
elif format == 0:
|
|
#flowed
|
|
#FIXME: following split should be regex to match \n\s*\n instead?
|
|
for line in markuptext.split('\n\n'):
|
|
self.start_paragraph(style_name)
|
|
#flowed, make normal whitespace go away
|
|
line = line.replace('\n',' ')
|
|
line = ' '.join(line.split())
|
|
self.__write_text(line, markup=True)
|
|
self.end_paragraph()
|
|
|
|
def __write_text(self, text, mark=None, markup=False):
|
|
"""
|
|
@param text: text to write.
|
|
@param mark: IndexMark to use for indexing (if supported)
|
|
@param markup: True if text already contains markup info.
|
|
Then text will no longer be escaped
|
|
"""
|
|
if not markup:
|
|
# We need to escape the text here for later pango.Layout.set_markup
|
|
# calls. This way we save the markup created by the report
|
|
# The markup in the note editor is not in the text so is not
|
|
# considered. It must be added by pango too
|
|
text = escape(text)
|
|
self._active_element.add_text(text)
|
|
|
|
def write_text(self, text, mark=None):
|
|
"""Write a normal piece of text according to the
|
|
present style
|
|
@param text: text to write.
|
|
@param mark: IndexMark to use for indexing (if supported)
|
|
"""
|
|
self.__write_text(text, mark)
|
|
|
|
def write_markup(self, text, s_tags):
|
|
"""
|
|
Writes the text in the current paragraph. Should only be used after a
|
|
start_paragraph and before an end_paragraph.
|
|
|
|
@param text: text to write. The text is assumed to be _not_ escaped
|
|
@param s_tags: assumed to be list of styledtexttags to apply to the
|
|
text
|
|
"""
|
|
markuptext = self._add_markup_from_styled(text, s_tags)
|
|
self.__write_text(text, markup=True)
|
|
|
|
def add_media_object(self, name, pos, x_cm, y_cm):
|
|
new_image = GtkDocPicture(pos, name, x_cm, y_cm)
|
|
self._active_element.add_child(new_image)
|
|
|
|
# DrawDoc implementation
|
|
|
|
def start_page(self):
|
|
# if this is not the first page we need to "close" the previous one
|
|
if self._doc.get_children():
|
|
self._doc.add_child(GtkDocPagebreak())
|
|
|
|
new_frame_style = FrameStyle(width=self.get_usable_width(),
|
|
height=self.get_usable_height())
|
|
new_frame = GtkDocFrame(new_frame_style)
|
|
|
|
self._active_element.add_child(new_frame)
|
|
self._active_element = new_frame
|
|
|
|
def end_page(self):
|
|
self._active_element = self._active_element.get_parent()
|
|
|
|
def draw_line(self, style_name, x1, y1, x2, y2):
|
|
style_sheet = self.get_style_sheet()
|
|
style = style_sheet.get_draw_style(style_name)
|
|
|
|
new_line = GtkDocLine(style, x1, y1, x2, y2)
|
|
self._active_element.add_child(new_line)
|
|
|
|
def draw_path(self, style_name, path):
|
|
style_sheet = self.get_style_sheet()
|
|
style = style_sheet.get_draw_style(style_name)
|
|
|
|
new_polygon = GtkDocPolygon(style, path)
|
|
self._active_element.add_child(new_polygon)
|
|
|
|
def draw_box(self, style_name, text, x, y, w, h):
|
|
# we handle the box and...
|
|
style_sheet = self.get_style_sheet()
|
|
style = style_sheet.get_draw_style(style_name)
|
|
|
|
new_box = GtkDocBox(style, x, y, w, h)
|
|
self._active_element.add_child(new_box)
|
|
|
|
# ...the text separately
|
|
paragraph_style_name = style.get_paragraph_style()
|
|
if paragraph_style_name:
|
|
paragraph_style = style_sheet.get_paragraph_style(paragraph_style_name)
|
|
paragraph_style.set_alignment(BaseDoc.PARA_ALIGN_LEFT)
|
|
|
|
# horizontal position of the text is not included in the style,
|
|
# we assume that it is the size of the shadow, or 0.2mm
|
|
if style.get_shadow():
|
|
x_offset = style.get_shadow_space()
|
|
else:
|
|
x_offset = 0.2
|
|
|
|
new_text = GtkDocText(paragraph_style, 'center', text,
|
|
x + x_offset , y + h / 2, angle=0)
|
|
self._active_element.add_child(new_text)
|
|
|
|
def draw_text(self, style_name, text, x, y):
|
|
style_sheet = self.get_style_sheet()
|
|
style = style_sheet.get_draw_style(style_name)
|
|
paragraph_style_name = style.get_paragraph_style()
|
|
paragraph_style = style_sheet.get_paragraph_style(paragraph_style_name)
|
|
paragraph_style.set_alignment(BaseDoc.PARA_ALIGN_LEFT)
|
|
|
|
new_text = GtkDocText(paragraph_style, 'top', text, x, y, angle=0)
|
|
self._active_element.add_child(new_text)
|
|
|
|
def center_text(self, style_name, text, x, y):
|
|
style_sheet = self.get_style_sheet()
|
|
style = style_sheet.get_draw_style(style_name)
|
|
paragraph_style_name = style.get_paragraph_style()
|
|
paragraph_style = style_sheet.get_paragraph_style(paragraph_style_name)
|
|
paragraph_style.set_alignment(BaseDoc.PARA_ALIGN_CENTER)
|
|
|
|
new_text = GtkDocText(paragraph_style, 'top', text, x, y, angle=0)
|
|
self._active_element.add_child(new_text)
|
|
|
|
def rotate_text(self, style_name, text, x, y, angle):
|
|
style_sheet = self.get_style_sheet()
|
|
style = style_sheet.get_draw_style(style_name)
|
|
paragraph_style_name = style.get_paragraph_style()
|
|
paragraph_style = style_sheet.get_paragraph_style(paragraph_style_name)
|
|
paragraph_style.set_alignment(BaseDoc.PARA_ALIGN_CENTER)
|
|
|
|
new_text = GtkDocText(paragraph_style, 'center', '\n'.join(text),
|
|
x, y, angle)
|
|
self._active_element.add_child(new_text)
|
|
|
|
# paginating and drawing interface
|
|
|
|
def run(self):
|
|
"""Create the physical output from the meta document.
|
|
|
|
It must be implemented in the subclasses. The idea is that with
|
|
different subclass different output could be generated:
|
|
e.g. Print, PDF, PS, PNG (which are currently supported by Cairo).
|
|
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
def paginate(self, layout, page_width, page_height, dpi_x, dpi_y):
|
|
"""Paginate the meta document in chunks.
|
|
|
|
Only one document level element is handled at one run.
|
|
|
|
"""
|
|
# if first time run than initialize the variables
|
|
if not self._elements_to_paginate:
|
|
self._elements_to_paginate = self._doc.get_children()[:]
|
|
self._pages.append(GtkDocDocument())
|
|
self._available_height = page_height
|
|
|
|
# try to fit the next element to current page, divide it if needed
|
|
elem = self._elements_to_paginate.pop(0)
|
|
(e1, e2), e1_h = elem.divide(layout,
|
|
page_width,
|
|
self._available_height,
|
|
dpi_x,
|
|
dpi_y)
|
|
|
|
# if (part of) it fits on current page add it
|
|
if e1 is not None:
|
|
self._pages[len(self._pages) - 1].add_child(e1)
|
|
|
|
# if elem was divided remember the second half to be processed
|
|
if e2 is not None:
|
|
self._elements_to_paginate.insert(0, e2)
|
|
|
|
# calculate how much space left on current page
|
|
self._available_height -= e1_h
|
|
|
|
# start new page if needed
|
|
if (e1 is None) or (e2 is not None):
|
|
self._pages.append(GtkDocDocument())
|
|
self._available_height = page_height
|
|
|
|
return len(self._elements_to_paginate) == 0
|
|
|
|
def draw_page(self, page_nr, cr, layout, width, height, dpi_x, dpi_y):
|
|
"""Draw a page on a Cairo context.
|
|
"""
|
|
if DEBUG:
|
|
cr.set_line_width(0.1)
|
|
cr.set_source_rgb(0, 1.0, 0)
|
|
cr.rectangle(0, 0, width, height)
|
|
cr.stroke()
|
|
|
|
self._pages[page_nr].draw(cr, layout, width, dpi_x, dpi_y)
|