diff --git a/qpageview/pdf.py b/qpageview/pdf.py index d94f737..7d0c19d 100644 --- a/qpageview/pdf.py +++ b/qpageview/pdf.py @@ -28,6 +28,7 @@ import platform from PyQt6.QtCore import Qt, QByteArray, QModelIndex, QRect, QRectF, QSize, QUrl +from PyQt6.QtGui import QPainter, QTransform from PyQt6.QtPdf import QPdfDocument, QPdfDocumentRenderOptions # Check for PDF link support (added in Qt 6.6) @@ -47,6 +48,7 @@ from . import link from . import locking from . import render +from . import util class Link(link.Link): @@ -218,8 +220,6 @@ 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. @@ -230,12 +230,32 @@ def tiles(self, width, height): """ yield render.Tile(0, 0, width, height) + def render(self, page, key, tile, paperColor=None): + """Generate a QImage for tile of the Page.""" + if key.rotation: + # this is only doable by drawing on a second QImage + return super().render(page, key, tile, paperColor) + else: + # reuse the QImage returned by QPdfDocument.render() to save memory + image = self._render(page, key, tile) + if paperColor: + painter = QPainter(image) + painter.setCompositionMode( + QPainter.CompositionMode.CompositionMode_DestinationOver) + painter.fillRect(image.rect(), paperColor) + return image + def draw(self, page, painter, key, tile, paperColor=None): """Draw a tile on the painter. The painter is already at the right position and rotation. """ + matrix = painter.deviceTransform() + painter.drawImage(0, 0, self._render(page, key, tile, matrix)) + + def _render(self, page, key, tile, matrix=None): + """The actual rendering logic shared by render() and draw().""" pageSize = page.pageSize() # in points # key and tile coordinates scale with device resolution and zoom level source = QRectF(0, 0, key.width, key.height) @@ -248,7 +268,8 @@ def draw(self, page, painter, key, tile, paperColor=None): num = page.pageNumber # We use this to scale from key/tile to device coordinates - matrix = painter.deviceTransform() + if matrix is None: + matrix = QTransform() # When we are displaying an image on screen, our painter coordinates # are "actual size" and scaling is our responsibility. When printing, @@ -259,15 +280,7 @@ def draw(self, page, painter, key, tile, paperColor=None): # Oversampling produces more readable output at lower resolutions # when painting at "actual size" - if actualSize: - # If our effective pixel density at this zoom level is below - # our threshold, render at double size then downscale - xres = 72.0 * key.width / pageSize.width() - yres = 72.0 * key.height / pageSize.height() - xMultiplier = 2 if xres < self.oversampleThreshold else 1 - yMultiplier = 2 if yres < self.oversampleThreshold else 1 - else: - xMultiplier = yMultiplier = 1 + multiplier = util.oversampleFactor(key, pageSize) if actualSize else 1 # Set rendering options RenderFlag = QPdfDocumentRenderOptions.RenderFlag @@ -283,7 +296,7 @@ def draw(self, page, painter, key, tile, paperColor=None): # Render the image at the output device's resolution (or double # that if we are oversampling) - s = matrix.scale(xMultiplier, yMultiplier).mapRect(source) + s = matrix.scale(multiplier, multiplier).mapRect(source) renderSize = QSize(int(s.width()), int(s.height())) with locking.lock(doc): image = doc.render(num, renderSize, renderOptions) @@ -298,7 +311,7 @@ def draw(self, page, painter, key, tile, paperColor=None): Qt.AspectRatioMode.IgnoreAspectRatio, Qt.TransformationMode.SmoothTransformation) - painter.drawImage(target, image, QRectF(image.rect())) + return image def load(source): diff --git a/qpageview/util.py b/qpageview/util.py index 26ecd21..c62d239 100644 --- a/qpageview/util.py +++ b/qpageview/util.py @@ -304,6 +304,31 @@ def autoCropRect(image): return QRegion(QBitmap.fromImage(mask)).boundingRect() +def oversampleFactor(key, pageSize): + """Return the optimal scale factor for oversampling. + + Oversampling (rendering the image at higher resolution, then + downscaling) can help with readability at lower pixel densities. + + The pageSize argument specifies the page dimensions in points as + returned by page.pageSize(). + + If this returns 1, render the image directly at your desired size. + Otherwise, multiply the page dimensions by the returned value before + rendering, then downscale to your desired size. + + """ + # the choice of threshold here is arbitrary, but was found to produce + # subjectively pleasing results across a variety of devices in testing + threshold = 96 # DPI of a standard PC screen + # calculate the effective pixel density at the current zoom level + # (key dimensions scale with zoom level, while pageSize is constant) + xres = 72.0 * key.width / pageSize.width() + yres = 72.0 * key.height / pageSize.height() + # recommend oversampling if we are below our threshold + return 2 if xres < threshold or yres < threshold else 1 + + def tempdir(): """Return a temporary directory that is erased on app quit.""" import tempfile