diff --git a/qpageview/pdf.py b/qpageview/pdf.py index a765ea8..cc4ed05 100644 --- a/qpageview/pdf.py +++ b/qpageview/pdf.py @@ -220,16 +220,7 @@ def document(self): class PdfRenderer(render.AbstractRenderer): oversampleThreshold = 96 # DPI of a standard PC screen - - def tiles(self, width, height): - """Yield four-tuples Tile(x, y, w, h) describing the tiles to render. - - For the QtPdf backend, this always returns a single tile covering - the entire page because QtPdf does not support selectively rendering - a smaller area. - - """ - yield render.Tile(0, 0, width, height) + renderFullPages = True # QtPDF doesn't support partial page rendering def draw(self, page, painter, key, tile, paperColor=None): """Draw a tile on the painter. @@ -303,10 +294,6 @@ def draw(self, page, painter, key, tile, paperColor=None): Qt.AspectRatioMode.IgnoreAspectRatio, Qt.TransformationMode.SmoothTransformation) - # Erase the target area and draw the image - painter.eraseRect(target) - if paperColor: - painter.fillRect(target, paperColor) painter.drawImage(target, image, QRectF(image.rect())) diff --git a/qpageview/render.py b/qpageview/render.py index 89d4b7a..a7fd435 100644 --- a/qpageview/render.py +++ b/qpageview/render.py @@ -112,6 +112,13 @@ class AbstractRenderer: # antialias True by default (not all renderers may support this) antialiasing = True + # qpageview was designed to render pages tile-by-tile. However, some + # backends like QtPDF only support full-page rendering, so for these we + # render the page first and divide it into tiles later. This is less + # efficient, but still provides certain performance advantages of using + # tiles like reduced flicker when repainting the widget. + renderFullPages = False + def __init__(self, cache=None): if cache: self.cache = cache @@ -219,17 +226,15 @@ def render(self, page, key, tile, paperColor=None): The default implementation prepares the image, a painter and then calls draw() to actually draw the contents. - If the paperColor is not specified, it will be read from the Page's - paperColor attribute (if not None) or else from the renderer's - paperColor attribute. + If the paperColor is not specified, the returned image will have a + transparent background, and the caller is responsible to paint or + erase the background as needed before calling this method. """ - if paperColor is None: - paperColor = page.paperColor or self.paperColor - i = QImage(tile.w, tile.h, self.imageFormat) - if paperColor: - i.fill(paperColor) + # leave the background transparent if no paperColor is specified, + # since we don't necessarily want to fill it e.g. when printing + i.fill(paperColor or Qt.GlobalColor.transparent) painter = QPainter(i) # rotate the painter accordingly @@ -371,6 +376,8 @@ def paint(self, page, painter, rect, callback=None): for (r, image, source) in reversed(images): # scale the target rect back to the paint device target = QRectF(r.x() / info.ratio, r.y() / info.ratio, r.width() / info.ratio, r.height() / info.ratio) + # job() already filled the background when rendering the page + # so we don't have to do that separately, which can cause flicker painter.drawImage(target, image, source) def schedule(self, page, key, tiles, callback): @@ -380,6 +387,14 @@ def schedule(self, page, key, tiles, callback): pending job. """ + if self.renderFullPages: + # ignore the caller's request and always render the full page; + # job() will do the actual division into tiles later + tiles = [Tile(0, 0, key.width, key.height)] + # ensure the cache can always hold at least two full pages to + # avoid excessive re-rendering at higher zoom levels + pageBytes = key.width * key.height * 4 # 32 bits == 4 bytes + self.cache.maxsize = max(cache.ImageCache.maxsize, pageBytes * 2) for tile in tiles: try: job = _jobs[(key, tile)] @@ -398,12 +413,19 @@ def job(self, page, key, tile): exception = [] def work(): try: - return self.render(page, key, tile) + # filling the background here is an optimization for paint() + return self.render(page, key, tile, + page.paperColor or self.paperColor) except Exception: exception.extend(sys.exc_info()) return QImage() def finalize(image): - self.cache.addtile(key, tile, image) + if self.renderFullPages: + for subtile in self.tiles(key.width, key.height): + self.cache.addtile(key, subtile, + image.copy(QRect(*map(int, subtile)))) + else: + self.cache.addtile(key, tile, image) for cb in callbacks: cb(page) del _jobs[(key, tile)]