# -*- coding: utf-8 -*-
#
# This file is part of the qpageview package.
#
# Copyright (c) 2019 - 2019 by Wilbert Berendsen
#
# 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 St, Fifth Floor, Boston, MA 02110-1301 USA
# See http://www.gnu.org/licenses/ for more information.
"""
A MultiPage has no contents itself (but it has a size!), and renders a list of
embedded pages.
The MultiPageRenderer has the same interface as an ordinary renderer, but defers
rendering to the renderer of the embedded pages.
"""
import collections
import itertools
from PyQt5.QtCore import QPoint, QRect, QRectF, Qt
from PyQt5.QtGui import QColor, QImage, QPainter, QPixmap, QRegion, QTransform
from . import document
from . import page
from . import render
[docs]class MultiPage(page.AbstractRenderedPage):
"""A special Page that has a list of embedded sub pages.
The sub pages are in the pages attribute, the first one is on top.
The position and size of the embedded pages is set in the updateSize()
method, which is inherited from AbstractPage. By default all sub pages
are centered in their natural size.
Rotation of sub pages is relative to the MultiPage.
The `scalePages` instance attribute can be used to multiply the zoomfactor
for the sub pages.
The `opaquePages` instance attribute optimizes some procedures when set to
True (i.e. it prevents rendering sub pages that are hidden below others).
By default, only links in the first sub page are handled.
Set `linksOnlyFirstSubPage` to False if you want links in all sub pages.
"""
scalePages = 1.0
opaquePages = True
linksOnlyFirstSubPage = True
def __init__(self, renderer=None):
self.pages = []
if renderer is not None:
self.renderer = renderer
[docs] @classmethod
def createPages(cls, pageLists, renderer=None, pad=page.BlankPage):
"""Yield pages, taking each page from every pageList.
If pad is given and is not None, it is a callable that instantiates
blank pages, to pad the shorter pageLists with. In that case, the
returned list of pages has the same length as the longest pageList
given. If pad is None, the returned list of pages has the same length
as the shortest pageList given.
"""
it = itertools.zip_longest(*pageLists) if pad else zip(*pageLists)
for pages in it:
page = cls(renderer)
page.pages[:] = (p if p else pad() for p in pages)
yield page
[docs] def copy(self, owner=None, matrix=None):
"""Reimplemented to also copy the sub pages."""
page = super().copy(owner, matrix)
page.pages = [p.copy(owner, matrix) for p in self.pages]
return page
[docs] def updateSize(self, dpiX, dpiY, zoomFactor):
"""Reimplemented to also position our sub-pages.
The default implementation of this method zooms the sub pages
at the zoom level of the page * self.scalePages.
"""
super().updateSize(dpiX, dpiY, zoomFactor)
# zoom the sub pages, using the same zoomFactor
for page in self.pages:
page.computedRotation = (page.rotation + self.computedRotation) & 3
page.updateSize(dpiX, dpiY, zoomFactor * self.scalePages)
self.updatePagePositions()
[docs] def updatePagePositions(self):
"""Called by updateSize(), set the page positions.
The default implementation of this method centers the pages.
"""
# center the pages
center = self.rect().center()
for page in self.pages:
r = page.rect()
r.moveCenter(center)
page.setGeometry(r)
[docs] def visiblePagesAt(self, rect):
"""Yield (page, rect) for all subpages.
The rect may be invalid when opaquePages is False. If opaquePages is
True, pages outside rect or hidden below others are exclued. The
yielded rect is always valid in that case.
"""
if not self.opaquePages:
for p in self.pages:
yield p, rect & p.geometry()
else:
covered = QRegion()
for p in self.pages:
overlayrect = rect & p.geometry()
if not overlayrect or not QRegion(overlayrect).subtracted(covered):
continue # skip if this part is hidden below the other
covered += overlayrect
yield p, overlayrect
if not QRegion(rect).subtracted(covered):
break
[docs] def printablePagesAt(self, rect):
"""Yield (page, matrix) for all subpages that are visible in rect.
If opaquePages is True, excludes pages outside rect or hidden below
others. The matrix (QTransform) describes the transformation from the
page to the sub page. Rect is in original coordinates, as with the
print() method.
"""
origmatrix = self.transform().inverted()[0] # map pos to original page
origmatrix.scale(self.scaleX, self.scaleY) # undo the scaling done in printing.py
for p, r in self.visiblePagesAt(self.mapToPage().rect(rect)):
center = origmatrix.map(QRectF(p.geometry()).center())
m = QTransform() # matrix from page to subpage
m.translate(center.x(), center.y())
m.rotate(p.rotation * 90) # rotation relative to us
m.scale(
self.scalePages * p.scaleX * self.dpi / p.dpi,
self.scalePages * p.scaleY * self.dpi / p.dpi)
m.translate(p.pageWidth / -2, p.pageHeight / -2)
yield p, m
[docs] def print(self, painter, rect=None, paperColor=None):
"""Prints our sub pages."""
if rect is None:
rect = self.pageRect()
else:
rect = rect & self.pageRect()
painter.translate(-rect.topLeft())
# print from bottom to top
for p, m in reversed(list(self.printablePagesAt(rect))):
# find center of the page corresponding to our center
painter.save()
painter.setTransform(m, True)
# handle rect clipping
clip = m.inverted()[0].mapRect(rect) & p.pageRect()
painter.fillRect(clip, paperColor or Qt.white) # draw a white background
painter.translate(clip.topLeft()) # the page will go back...
p.print(painter, clip)
painter.restore()
[docs] def text(self, rect):
"""Reimplemented to get text from sub pages."""
for p, rect in self.visiblePagesAt(rect):
if rect:
text = p.text(rect.translated(-p.pos()))
if text:
return text
def _linkPages(self, rect=None):
"""Internal. Yield the pages allowed for links (and visible in rect if given)."""
for p, rect in self.visiblePagesAt(rect or self.rect()):
yield p, rect
if self.linksOnlyFirstSubPage:
break
[docs] def linksAt(self, point):
"""Reimplemented to find links in sub pages."""
result = []
for p, rect in self._linkPages():
if point in rect:
result.extend(p.linksAt(point - p.pos()))
return result
[docs] def linksIn(self, rect):
"""Reimplemented to find links in sub pages."""
result = set()
for p, rect in self._linkPages(rect):
result.update(p.linksIn(rect.translated(-p.pos())))
return result
[docs] def linkRect(self, link):
"""Reimplemented to get correct area on the page the link belongs to."""
for p, r in self._linkPages():
if link in p.links():
return p.linkRect(link).translated(p.pos())
return QRect() # just in case
[docs]class MultiPageDocument(document.MultiSourceDocument):
"""A Document that combines pages from different documents."""
pageClass = MultiPage
[docs] def createPages(self):
pageLists = [[p.copy() for p in doc.pages()] for doc in self.sources()]
return self.pageClass.createPages(pageLists, self.renderer)
[docs]class CallBack:
"""A wrapper for a callable that is called with the original Page."""
def __new__(cls, origcallable, page):
# if the callable is already a CallBack instance, just return it. This
# would happen if a MultiPage has a subpage that is also a MultiPage.
if cls == type(origcallable):
return origcallable
cb = object.__new__(cls)
cb.origcallable = origcallable
cb.page = page
return cb
def __hash__(self):
"""Return the hash of the original callable.
This way only one callback will be in the Job.callbacks attribute,
despite of multiple pages, and unscheduling a job with subpages still
works.
"""
return hash(self.origcallable)
def __call__(self, page):
"""Call the original callback with the original Page."""
self.origcallable(self.page)
# install a default renderer, so MultiPage can be used directly
MultiPage.renderer = MultiPageRenderer()