-
Notifications
You must be signed in to change notification settings - Fork 14
Optimize PDF rendering to save memory #58
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For clarification: the default implementation in render.py creates a new QImage, fills its background if needed, then calls We do need two images when performing manipulations like rotating the page, which is implemented by using QPainter to draw the content rotated on top of the background image. |
||
| 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): | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -304,6 +304,31 @@ def autoCropRect(image): | |
| return QRegion(QBitmap.fromImage(mask)).boundingRect() | ||
|
|
||
|
|
||
| def oversampleFactor(key, pageSize): | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought I was going to have to duplicate a lot more code when I refactored the rendering logic. That turned out not to be the case, but separating this out seemed like a good idea anyway for code clarity and reuse. |
||
| """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 | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know, I know. :)