forked from openlp/openlp
589 lines
22 KiB
Python
589 lines
22 KiB
Python
# -*- coding: utf-8 -*-
|
|
# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4
|
|
"""
|
|
OpenLP - Open Source Lyrics Projection
|
|
Copyright (c) 2008 Raoul Snyman
|
|
Portions copyright (c) 2008-2009 Martin Thompson, Tim Bentley
|
|
|
|
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; version 2 of the License.
|
|
|
|
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
|
|
"""
|
|
import logging
|
|
import os
|
|
import sys
|
|
|
|
from PyQt4 import QtGui, QtCore
|
|
|
|
class Renderer(object):
|
|
"""
|
|
Genarates a pixmap image of a array of text. The Text is formatted to
|
|
make sure it fits on the screen and if not extra frames a generated.
|
|
"""
|
|
global log
|
|
log = logging.getLogger(u'Renderer')
|
|
log.info(u'Renderer Loaded')
|
|
|
|
def __init__(self):
|
|
"""
|
|
Initialise the renderer.
|
|
"""
|
|
self._rect = None
|
|
self._debug = 0
|
|
self._right_margin = 64 # the amount of right indent
|
|
self._shadow_offset = 5
|
|
self._shadow_offset_footer = 3
|
|
self._outline_offset = 2
|
|
self.theme_name = None
|
|
self._theme = None
|
|
self._bg_image_filename = None
|
|
self._frame = None
|
|
self._bg_frame = None
|
|
self.bg_image = None
|
|
self._bg_frame_small = None
|
|
|
|
def set_debug(self, debug):
|
|
"""
|
|
Set the debug mode of the renderer.
|
|
|
|
``debug``
|
|
The debug mode.
|
|
"""
|
|
self._debug=debug
|
|
|
|
def set_theme(self, theme):
|
|
"""
|
|
Set the theme to be used.
|
|
|
|
``theme``
|
|
The theme to be used.
|
|
"""
|
|
log.debug(u'set theme')
|
|
self._theme = theme
|
|
self._bg_frame = None
|
|
self.bg_image = None
|
|
self.theme_name = theme.theme_name
|
|
self._set_theme_font()
|
|
if theme.background_type == u'image':
|
|
if theme.background_filename is not None:
|
|
self.set_bg_image(theme.background_filename)
|
|
|
|
def set_bg_image(self, filename):
|
|
"""
|
|
Set a background image.
|
|
|
|
``filename``
|
|
The name of the image file.
|
|
"""
|
|
log.debug(u'set bg image %s', filename)
|
|
self._bg_image_filename = unicode(filename)
|
|
if self._frame is not None:
|
|
self.scale_bg_image()
|
|
|
|
def scale_bg_image(self):
|
|
"""
|
|
Scale the background image to fit the screen.
|
|
"""
|
|
assert self._frame
|
|
preview = QtGui.QImage(self._bg_image_filename)
|
|
width = self._frame.width()
|
|
height = self._frame.height()
|
|
preview = preview.scaled(width, height, QtCore.Qt.KeepAspectRatio,
|
|
QtCore.Qt.SmoothTransformation)
|
|
realwidth = preview.width()
|
|
realheight = preview.height()
|
|
# and move it to the centre of the preview space
|
|
self.bg_image = QtGui.QImage(width, height,
|
|
QtGui.QImage.Format_ARGB32_Premultiplied)
|
|
self.bg_image.fill(QtCore.Qt.black)
|
|
painter = QtGui.QPainter()
|
|
painter.begin(self.bg_image)
|
|
self.background_offsetx = (width - realwidth) / 2
|
|
self.background_offsety = (height - realheight) / 2
|
|
painter.drawImage(self.background_offsetx, self.background_offsety,
|
|
preview)
|
|
painter.end()
|
|
|
|
def set_frame_dest(self, frame_width, frame_height, preview=False):
|
|
"""
|
|
Set the size of the slide.
|
|
|
|
``frame_width``
|
|
The width of the slide.
|
|
|
|
``frame_height``
|
|
The height of the slide.
|
|
|
|
``preview``
|
|
Defaults to *False*. Whether or not to generate a preview.
|
|
"""
|
|
if preview == True:
|
|
self._bg_frame = None
|
|
log.debug(u'set frame dest (frame) w %d h %d', frame_width,
|
|
frame_height)
|
|
self._frame = QtGui.QImage(frame_width, frame_height,
|
|
QtGui.QImage.Format_ARGB32_Premultiplied)
|
|
if self._bg_image_filename is not None and self.bg_image is None:
|
|
self.scale_bg_image()
|
|
if self._bg_frame is None:
|
|
self._generate_background_frame()
|
|
|
|
def format_slide(self, words, footer):
|
|
"""
|
|
Figure out how much text can appear on a slide, using the current
|
|
theme settings.
|
|
|
|
``words``
|
|
The words to be fitted on the slide.
|
|
|
|
``footer``
|
|
The footer of the slide.
|
|
"""
|
|
log.debug(u'format_slide - Start')
|
|
# print words
|
|
verses = []
|
|
words = words.replace(u'\r\n', u'\n')
|
|
verses_text = words.split(u'\n')
|
|
#print verses_text
|
|
text = []
|
|
for verse in verses_text:
|
|
lines = verse.split(u'\n')
|
|
for line in lines:
|
|
text.append(line)
|
|
split_text = self.pre_render_text(text)
|
|
# print split_text
|
|
log.debug(u'format_slide - End')
|
|
return split_text
|
|
|
|
def pre_render_text(self, text):
|
|
metrics = QtGui.QFontMetrics(self.mainFont)
|
|
#take the width work out approx how many characters and add 50%
|
|
line_width = self._rect.width() - self._right_margin
|
|
#number of lines on a page - adjust for rounding up.
|
|
# print "Metrics ", line_width
|
|
page_length = int(self._rect.height() / metrics.height() - 2 ) - 1
|
|
ave_line_width = line_width / metrics.averageCharWidth()
|
|
ave_line_width = int(ave_line_width + (ave_line_width * 1))
|
|
# print "B", ave_line_width
|
|
split_pages = []
|
|
page = []
|
|
split_lines = []
|
|
count = 0
|
|
for line in text:
|
|
#print "C", line, len(line)
|
|
#Must be a blank line so keep it.
|
|
if len(line) == 0:
|
|
line = u' '
|
|
while len(line) > 0:
|
|
# print "C1", line, len(line)
|
|
if len(line) > ave_line_width:
|
|
pos = line.find(u' ', ave_line_width)
|
|
split_text = line[:pos]
|
|
else:
|
|
pos = len(line)
|
|
split_text = line
|
|
# print "E", metrics.width(split_text, -1), line_width
|
|
while metrics.width(split_text, -1) > line_width:
|
|
#Find the next space to the left
|
|
pos = line[:pos].rfind(u' ')
|
|
# print "F", pos, line[:pos]
|
|
#no more spaces found
|
|
if pos == 0:
|
|
split_text = line
|
|
while metrics.width(split_text, -1) > line_width:
|
|
split_text = split_text[:-1]
|
|
pos = len(split_text)
|
|
else:
|
|
split_text = line[:pos]
|
|
# print "F1", split_text, line, pos
|
|
split_lines.append(split_text)
|
|
line = line[pos:]
|
|
#Text fits in a line now
|
|
# if len(line) <= line_width:
|
|
# split_lines.append(line)
|
|
# line = u''
|
|
# print "G", split_lines
|
|
# print "H", line
|
|
#print "I", split_lines, page_length
|
|
for line in split_lines:
|
|
page.append(line)
|
|
if len(page) == page_length:
|
|
split_pages.append(page)
|
|
page = []
|
|
if len(page) > 0:
|
|
split_pages.append(page)
|
|
return split_pages
|
|
|
|
def set_text_rectangle(self, rect_main, rect_footer):
|
|
"""
|
|
Sets the rectangle within which text should be rendered.
|
|
|
|
``rect_main``
|
|
The main text block.
|
|
|
|
``rect_footer``
|
|
The footer text block.
|
|
"""
|
|
self._rect = rect_main
|
|
self._rect_footer = rect_footer
|
|
|
|
def generate_frame_from_lines(self, lines, footer_lines=None):
|
|
"""
|
|
Render a set of lines according to the theme, and return the block
|
|
dimensions.
|
|
|
|
``lines``
|
|
The lines to be rendered.
|
|
|
|
``footer_lines``
|
|
Defaults to *None*. The footer to render.
|
|
"""
|
|
log.debug(u'generate_frame_from_lines - Start')
|
|
#print "Render Lines ", lines
|
|
bbox = self._render_lines_unaligned(lines, False)
|
|
if footer_lines is not None:
|
|
bbox1 = self._render_lines_unaligned(footer_lines, True)
|
|
# reset the frame. first time do not worry about what you paint on.
|
|
self._frame = QtGui.QImage(self._bg_frame)
|
|
x, y = self._correctAlignment(self._rect, bbox)
|
|
bbox = self._render_lines_unaligned(lines, False, (x, y), True)
|
|
if footer_lines is not None:
|
|
bbox = self._render_lines_unaligned(footer_lines, True,
|
|
(self._rect_footer.left(), self._rect_footer.top()), True)
|
|
log.debug(u'generate_frame_from_lines - Finish')
|
|
return self._frame
|
|
|
|
def _generate_background_frame(self):
|
|
"""
|
|
Generate a background frame to the same size as the frame to be used.
|
|
Results are cached for performance reasons.
|
|
"""
|
|
assert(self._theme)
|
|
self._bg_frame = QtGui.QImage(self._frame.width(), self._frame.height(),
|
|
QtGui.QImage.Format_ARGB32_Premultiplied)
|
|
log.debug(u'render background %s start', self._theme.background_type)
|
|
painter = QtGui.QPainter()
|
|
painter.begin(self._bg_frame)
|
|
if self._theme.background_mode == u'transparent':
|
|
painter.fillRect(self._frame.rect(), QtCore.Qt.transparent)
|
|
else:
|
|
if self._theme.background_type == u'solid':
|
|
painter.fillRect(self._frame.rect(),
|
|
QtGui.QColor(self._theme.background_color))
|
|
elif self._theme.background_type == u'gradient':
|
|
# gradient
|
|
gradient = None
|
|
if self._theme.background_direction == u'horizontal':
|
|
w = int(self._frame.width()) / 2
|
|
# vertical
|
|
gradient = QtGui.QLinearGradient(w, 0, w,
|
|
self._frame.height())
|
|
elif self._theme.background_direction == u'vertical':
|
|
h = int(self._frame.height()) / 2
|
|
# Horizontal
|
|
gradient = QtGui.QLinearGradient(0, h, self._frame.width(),
|
|
h)
|
|
else:
|
|
w = int(self._frame.width()) / 2
|
|
h = int(self._frame.height()) / 2
|
|
# Circular
|
|
gradient = QtGui.QRadialGradient(w, h, w)
|
|
gradient.setColorAt(0,
|
|
QtGui.QColor(self._theme.background_startColor))
|
|
gradient.setColorAt(1,
|
|
QtGui.QColor(self._theme.background_endColor))
|
|
painter.setBrush(QtGui.QBrush(gradient))
|
|
rectPath = QtGui.QPainterPath()
|
|
max_x = self._frame.width()
|
|
max_y = self._frame.height()
|
|
rectPath.moveTo(0, 0)
|
|
rectPath.lineTo(0, max_y)
|
|
rectPath.lineTo(max_x, max_y)
|
|
rectPath.lineTo(max_x, 0)
|
|
rectPath.closeSubpath()
|
|
painter.drawPath(rectPath)
|
|
elif self._theme.background_type== u'image':
|
|
# image
|
|
painter.fillRect(self._frame.rect(), QtCore.Qt.black)
|
|
if self.bg_image is not None:
|
|
painter.drawImage(0, 0, self.bg_image)
|
|
painter.end()
|
|
self._bg_frame_small = self._bg_frame.scaled(QtCore.QSize(280, 210),
|
|
QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
|
|
log.debug(u'render background End')
|
|
|
|
def _correctAlignment(self, rect, bbox):
|
|
"""
|
|
Corrects the vertical alignment of text.
|
|
|
|
``rect``
|
|
The block dimentions.
|
|
|
|
``bbox``
|
|
Footer dimensions?
|
|
"""
|
|
x = rect.left()
|
|
if int(self._theme.display_verticalAlign) == 0:
|
|
# top align
|
|
y = rect.top()
|
|
elif int(self._theme.display_verticalAlign) == 2:
|
|
# bottom align
|
|
y = rect.bottom() - bbox.height()
|
|
elif int(self._theme.display_verticalAlign) == 1:
|
|
# centre align
|
|
y = rect.top() + (rect.height() - bbox.height()) / 2
|
|
else:
|
|
log.error(u'Invalid value for theme.VerticalAlign:%s' % self._theme.display_verticalAlign)
|
|
return x, y
|
|
|
|
def _render_lines_unaligned(self, lines, footer, tlcorner=(0, 0),
|
|
live=False):
|
|
"""
|
|
Given a list of lines to render, render each one in turn (using the
|
|
``_render_single_line`` fn - which may result in going off the bottom).
|
|
They are expected to be pre-arranged to less than a screenful (eg. by
|
|
using split_set_of_lines).
|
|
|
|
Returns the bounding box of the text as QRect.
|
|
|
|
``lines``
|
|
The lines of text to render.
|
|
|
|
``footer``
|
|
The slide footer.
|
|
|
|
``tlcorner``
|
|
Defaults to *``(0, 0)``*. Co-ordinates of the top left corner.
|
|
|
|
``live``
|
|
Defaults to *False*. Whether or not this is a live screen.
|
|
"""
|
|
x, y = tlcorner
|
|
brx = x
|
|
bry = y
|
|
for line in lines:
|
|
# render after current bottom, but at original left edge
|
|
# keep track of right edge to see which is biggest
|
|
(thisx, bry) = self._render_and_wrap_single_line(line, footer,
|
|
(x, bry), live)
|
|
if (thisx > brx):
|
|
brx = thisx
|
|
retval = QtCore.QRect(x, y, brx - x, bry - y)
|
|
if self._debug:
|
|
painter = QtGui.QPainter()
|
|
painter.begin(self._frame)
|
|
painter.setPen(QtGui.QPen(QtGui.QColor(0, 0, 255)))
|
|
painter.drawRect(retval)
|
|
painter.end()
|
|
return retval
|
|
|
|
def _render_and_wrap_single_line(self, line, footer, tlcorner=(0, 0),
|
|
live=False):
|
|
"""
|
|
Render a single line of words onto the DC, top left corner specified.
|
|
If the line is too wide for the context, it wraps, but right-aligns
|
|
the surplus words in the manner of song lyrics.
|
|
|
|
Returns the bottom-right corner (of what was rendered) as a tuple(x, y).
|
|
|
|
``line``
|
|
Line of text to be rendered.
|
|
|
|
``footer``
|
|
The footer of the slide.
|
|
|
|
``tlcorner``
|
|
Defaults to *``(0, 0)``*. The top left corner.
|
|
|
|
``live``
|
|
Defaults to *False*. Whether or not this is a live screen.
|
|
"""
|
|
x, y = tlcorner
|
|
maxx = self._rect.width();
|
|
maxy = self._rect.height();
|
|
lines = []
|
|
lines.append(line)
|
|
startx = x
|
|
starty = y
|
|
rightextent = None
|
|
#print "inputs", startx, starty, maxx, maxy
|
|
# dont allow alignment messing with footers
|
|
if footer:
|
|
align = 0
|
|
shadow_offset = self._shadow_offset_footer
|
|
else:
|
|
align = int(self._theme .display_horizontalAlign)
|
|
shadow_offset = self._shadow_offset
|
|
#print lines
|
|
for linenum in range(len(lines)):
|
|
line = lines[linenum]
|
|
#find out how wide line is
|
|
w, h = self._get_extent_and_render(line, footer, tlcorner=(x, y),
|
|
draw=False)
|
|
if self._theme.display_shadow:
|
|
w += shadow_offset
|
|
h += shadow_offset
|
|
if self._theme.display_outline:
|
|
# pixels either side
|
|
w += 2 * self._outline_offset
|
|
# pixels top/bottom
|
|
h += 2 * self._outline_offset
|
|
if align == 0: # left align
|
|
rightextent = x + w
|
|
# shift right from last line's rh edge
|
|
if self._theme.display_wrapStyle == 1 and linenum != 0:
|
|
rightextent = self._first_line_right_extent +
|
|
self._right_margin
|
|
if rightextent > maxx:
|
|
rightextent = maxx
|
|
x = rightextent - w
|
|
# right align
|
|
elif align == 1:
|
|
rightextent = maxx
|
|
x = maxx - w
|
|
# centre
|
|
elif align == 2:
|
|
x = (maxx - w) / 2;
|
|
rightextent = x + w
|
|
if live:
|
|
# now draw the text, and any outlines/shadows
|
|
if self._theme.display_shadow:
|
|
self._get_extent_and_render(line, footer,
|
|
tlcorner=(x + shadow_offset, y + shadow_offset),
|
|
draw=True, color = self._theme.display_shadow_color)
|
|
if self._theme.display_outline:
|
|
self._get_extent_and_render(line, footer,
|
|
(x+self._outline_offset, y), draw=True,
|
|
color = self._theme.display_outline_color)
|
|
self._get_extent_and_render(line, footer,
|
|
(x, y+self._outline_offset), draw=True,
|
|
color = self._theme.display_outline_color)
|
|
self._get_extent_and_render(line, footer,
|
|
(x, y-self._outline_offset), draw=True,
|
|
color = self._theme.display_outline_color)
|
|
self._get_extent_and_render(line, footer,
|
|
(x-self._outline_offset, y), draw=True,
|
|
color = self._theme.display_outline_color)
|
|
if self._outline_offset > 1:
|
|
self._get_extent_and_render(line, footer,
|
|
(x+self._outline_offset, y+self._outline_offset),
|
|
draw=True,
|
|
color = self._theme.display_outline_color)
|
|
self._get_extent_and_render(line, footer,
|
|
(x-self._outline_offset, y+self._outline_offset),
|
|
draw=True,
|
|
color = self._theme.display_outline_color)
|
|
self._get_extent_and_render(line, footer,
|
|
(x+self._outline_offset, y-self._outline_offset),
|
|
draw=True,
|
|
color = self._theme.display_outline_color)
|
|
self._get_extent_and_render(line, footer,
|
|
(x-self._outline_offset, y-self._outline_offset),
|
|
draw=True,
|
|
color = self._theme.display_outline_color)
|
|
self._get_extent_and_render(line, footer,tlcorner=(x, y),
|
|
draw=True)
|
|
y += h
|
|
if linenum == 0:
|
|
self._first_line_right_extent = rightextent
|
|
# draw a box around the text - debug only
|
|
if self._debug:
|
|
painter = QtGui.QPainter()
|
|
painter.begin(self._frame)
|
|
painter.setPen(QtGui.QPen(QtGui.QColor(0,255,0)))
|
|
painter.drawRect(startx, starty, rightextent-startx, y-starty)
|
|
painter.end()
|
|
brcorner = (rightextent, y)
|
|
return brcorner
|
|
|
|
def _set_theme_font(self):
|
|
"""
|
|
Set the fonts from the current theme settings.
|
|
"""
|
|
footer_weight = 50
|
|
if self._theme.font_footer_weight == u'Bold':
|
|
footer_weight = 75
|
|
self.footerFont = QtGui.QFont(self._theme.font_footer_name,
|
|
int(self._theme.font_footer_proportion), # size
|
|
int(footer_weight), # weight
|
|
self._theme.font_footer_italics)# italic
|
|
self.footerFont.setPixelSize(int(self._theme.font_footer_proportion))
|
|
main_weight = 50
|
|
if self._theme.font_main_weight == u'Bold':
|
|
main_weight = 75
|
|
self.mainFont = QtGui.QFont(self._theme.font_main_name,
|
|
int(self._theme.font_main_proportion), # size
|
|
int(main_weight), # weight
|
|
self._theme.font_main_italics)# italic
|
|
self.mainFont.setPixelSize(int(self._theme.font_main_proportion))
|
|
|
|
def _get_extent_and_render(self, line, footer, tlcorner=(0, 0), draw=False,
|
|
color=None):
|
|
"""
|
|
Find bounding box of text - as render_single_line. If draw is set,
|
|
actually draw the text to the current DC as well return width and
|
|
height of text as a tuple (w, h).
|
|
|
|
``line``
|
|
The line of text to render.
|
|
|
|
``footer``
|
|
The footer text.
|
|
|
|
``tlcorner``
|
|
Defaults to *``(0, 0)``*. The top left corner co-ordinates.
|
|
|
|
``draw``
|
|
Defaults to *False*. Draw the text to the current surface.
|
|
|
|
``color``
|
|
Defaults to *None*. The colour to draw with.
|
|
"""
|
|
# setup defaults
|
|
painter = QtGui.QPainter()
|
|
painter.begin(self._frame)
|
|
if footer :
|
|
font = self.footerFont
|
|
else:
|
|
font = self.mainFont
|
|
painter.setFont(font)
|
|
if color is None:
|
|
if footer:
|
|
painter.setPen(QtGui.QColor(self._theme.font_footer_color))
|
|
else:
|
|
painter.setPen(QtGui.QColor(self._theme.font_main_color))
|
|
else:
|
|
painter.setPen(QtGui.QColor(color))
|
|
x, y = tlcorner
|
|
metrics = QtGui.QFontMetrics(font)
|
|
w = metrics.width(line)
|
|
h = metrics.height() - 2
|
|
if draw:
|
|
painter.drawText(x, y + metrics.ascent(), line)
|
|
painter.end()
|
|
return (w, h)
|
|
|
|
def snoop_Image(self, image, image2=None):
|
|
"""
|
|
Debugging method to allow images to be viewed.
|
|
|
|
``image``
|
|
An image to save to disk.
|
|
|
|
``image2``
|
|
Defaults to *None*. Another image to save to disk.
|
|
"""
|
|
im = image.toImage()
|
|
im.save(u'renderer.png', u'png')
|
|
if image2 is not None:
|
|
im = image2.toImage()
|
|
im.save(u'renderer2.png', u'png')
|