Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-qpageview for openSUSE:Factory checked in at 2026-10-20 12:38:19 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-qpageview (Old) and /work/SRC/openSUSE:Factory/.python-qpageview.new.3698736 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-qpageview" Tue Oct 20 12:38:19 2026 rev:4 rq:1386153 version:1.0.5 Changes: -------- --- /work/SRC/openSUSE:Factory/python-qpageview/python-qpageview.changes 2026-03-19 17:40:46.218244709 +0100 +++ /work/SRC/openSUSE:Factory/.python-qpageview.new.3698736/python-qpageview.changes 2026-10-20 12:39:12.344707853 +0200 @@ -1,0 +2,18 @@ +Sat Oct 17 16:45:29 UTC 2026 - Martin Pluskal <[email protected]> + +- Update to 1.0.5: + * Paint the background only when necessary (also fixes + rendering images with a transparent background). + * Optimize the cache size to avoid unnecessary re-rendering. + * Optimize PDF rendering to save memory. + * Fix a memory leak caused by uncollected QPdfDocument garbage. + +------------------------------------------------------------------- +Sun Apr 12 19:54:00 UTC 2026 - Dirk Müller <[email protected]> + +- update to 1.0.4: + * Optimize PDF link caching (#49) and reduce memory usage. + * Optimize PDF rendering (#50) to increase performance and + reduce memory usage. + +------------------------------------------------------------------- Old: ---- qpageview-1.0.3.tar.gz New: ---- qpageview-1.0.5.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-qpageview.spec ++++++ --- /var/tmp/diff_new_pack.dqKWzI/_old 2026-10-20 12:39:13.145741378 +0200 +++ /var/tmp/diff_new_pack.dqKWzI/_new 2026-10-20 12:39:13.147741462 +0200 @@ -18,9 +18,9 @@ %{?!python_module:%define python_module() python3-%{**}} Name: python-qpageview -Version: 1.0.3 +Version: 1.0.5 Release: 0 -Summary: Widget to display page-based documents for Qt5/PyQt5 +Summary: Widget to display page-based documents for Qt6/PyQt6 License: GPL-3.0-only URL: https://github.com/frescobaldi/qpageview Source: https://files.pythonhosted.org/packages/source/q/qpageview/qpageview-%{version}.tar.gz ++++++ qpageview-1.0.3.tar.gz -> qpageview-1.0.5.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qpageview-1.0.3/CHANGELOG.md new/qpageview-1.0.5/CHANGELOG.md --- old/qpageview-1.0.3/CHANGELOG.md 2020-02-02 01:00:00.000000000 +0100 +++ new/qpageview-1.0.5/CHANGELOG.md 2020-02-02 01:00:00.000000000 +0100 @@ -7,6 +7,28 @@ All notable changes to the qpageview project are documented in this file. +## [1.0.5] - 2026-05-27 + +### Fixed + +* Paint the background once and only when necessary to do so (#54). +For example, it's not necessary or desirable to paint the +background when printing. +Another consequence of this fix is that it's possible now to +render an image with a transparent background, for example in +Frescobaldi's copy-to-image dialog. +* Optimize the cache size to avoid unnecessary re-rendering (#56) +* Optimize PDF rendering to save memory (#58) +* Fix a memory leak caused by uncollected QPdfDocument garbage (#52) + + +## [1.0.4] - 2026-04-04 + +### Fixed + +* Optimize PDF link caching (#49) and reduce memory usage. +* Optimize PDF rendering (#50) to increase performance and reduce memory usage. + ## [1.0.3] - 2026-01-23 ### Fixed @@ -107,3 +129,5 @@ [1.0.1]: https://github.com/frescobaldi/qpageview/compare/v1.0.0...v1.0.1 [1.0.2]: https://github.com/frescobaldi/qpageview/compare/v1.0.1...v1.0.2 [1.0.3]: https://github.com/frescobaldi/qpageview/compare/v1.0.2...v1.0.3 +[1.0.4]: https://github.com/frescobaldi/qpageview/compare/v1.0.3...v1.0.4 +[1.0.5]: https://github.com/frescobaldi/qpageview/compare/v1.0.4...v1.0.5 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qpageview-1.0.3/PKG-INFO new/qpageview-1.0.5/PKG-INFO --- old/qpageview-1.0.3/PKG-INFO 2020-02-02 01:00:00.000000000 +0100 +++ new/qpageview-1.0.5/PKG-INFO 2020-02-02 01:00:00.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: qpageview -Version: 1.0.3 +Version: 1.0.5 Summary: Widget to display page-based documents for Qt6/PyQt6 Project-URL: Homepage, https://github.com/frescobaldi/qpageview Project-URL: Documentation, https://qpageview.org diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qpageview-1.0.3/qpageview/cache.py new/qpageview-1.0.5/qpageview/cache.py --- old/qpageview-1.0.3/qpageview/cache.py 2020-02-02 01:00:00.000000000 +0100 +++ new/qpageview-1.0.5/qpageview/cache.py 2020-02-02 01:00:00.000000000 +0100 @@ -69,6 +69,28 @@ except KeyError: return {} + def prepare(self, tiles): + """Make room before rendering new tiles. + + This ensures there is space for the new tiles by adjusting + the cache size and/or purging older images as needed. + + """ + # approximate size of the new tiles at 32 bits (== 4 bytes) per pixel + tileBytes = sum(tile.w * tile.h for tile in tiles) * 4 + + # adjust the cache size to fit at least two pages' visible tiles + # to avoid re-rendering either page when both are displayed + oldsize = self.maxsize + self.maxsize = max(type(self).maxsize, 2 * tileBytes) + if self.maxsize < oldsize: + # clear everything when shrinking the cache because otherwise + # larger tiles might prove difficult to purge() + self.clear() + else: + # free memory from unneeded tiles before rendering the new ones + self.purge(tileBytes) + def addtile(self, key, tile, image): """Add image for the specified key and tile.""" d = self._cache.setdefault(key.group, {}).setdefault(key.ident, {}).setdefault(key[2:], {}) @@ -77,17 +99,20 @@ except KeyError: pass - purgeneeded = self.currentsize > self.maxsize - e = d[tile] = ImageEntry(image) self.currentsize += e.bcount - if not purgeneeded: + def purge(self, reservedsize=0): + """Purge old images if needed. + + At least `reservedsize` bytes will remain available. + + """ + limit = self.maxsize - reservedsize + if self.currentsize <= limit: return - # purge old images is needed, # cache groups may have disappeared so count all images - entries = iter(sorted( ((entry.time, entry.bcount, group, ident, key, tile) for group, identd in self._cache.items() @@ -96,11 +121,11 @@ for tile, entry in tiled.items()), key=(lambda item: item[:2]), reverse=True)) - # now count the newest images until maxsize ... + # now count the newest images until our limit ... currentsize = 0 for time, bcount, group, ident, key, tile in entries: currentsize += bcount - if currentsize > self.maxsize: + if currentsize >= limit: break self.currentsize = currentsize # ... and delete the remaining images, deleting empty dicts as well diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qpageview-1.0.3/qpageview/export.py new/qpageview-1.0.5/qpageview/export.py --- old/qpageview-1.0.3/qpageview/export.py 2020-02-02 01:00:00.000000000 +0100 +++ new/qpageview-1.0.5/qpageview/export.py 2020-02-02 01:00:00.000000000 +0100 @@ -243,11 +243,25 @@ res = self.resolution if self.oversample != 1: res *= self.oversample - i = self.page().image(self._rect, res, res, self.paperColor) + if self.grayscale: + # convertToFormat() does weird things with the alpha channel, + # so we always render grayscale images with a solid background + # and handle transparent areas manually if needed + paperColor = self.paperColor or Qt.GlobalColor.white + else: + paperColor = self.paperColor + i = self.page().image(self._rect, res, res, paperColor) if self.oversample != 1: i = i.scaled(i.size() / self.oversample, transformMode=Qt.TransformationMode.SmoothTransformation) if self.grayscale: i = i.convertToFormat(QImage.Format.Format_Grayscale8) + if self.paperColor is None: + # restore the original transparent background + # since our alpha map here is just this image's negative, + # we save memory by doing this instead of making a copy() + i.invertPixels(QImage.InvertMode.InvertRgb) + i.setAlphaChannel(i) + i.invertPixels(QImage.InvertMode.InvertRgb) if self.autocrop: i = i.copy(util.autoCropRect(i)) # needed for correct resolution metadata; see issue #44 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qpageview-1.0.3/qpageview/pdf.py new/qpageview-1.0.5/qpageview/pdf.py --- old/qpageview-1.0.3/qpageview/pdf.py 2020-02-02 01:00:00.000000000 +0100 +++ new/qpageview-1.0.5/qpageview/pdf.py 2020-02-02 01:00:00.000000000 +0100 @@ -25,29 +25,38 @@ """ -import weakref import platform -from PyQt6.QtCore import Qt, QByteArray, QCoreApplication, QModelIndex, QRect, QRectF, QSize, QUrl -from PyQt6.QtGui import QPainter -from PyQt6.QtPdf import QPdfDocument, QPdfDocumentRenderOptions, QPdfLinkModel +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) +# As of 2026, some Linux distros still ship older Qt versions without it. +# We will attempt to run with point-and-click disabled on such systems. +try: + from PyQt6.QtPdf import QPdfLinkModel +except ImportError: + QPdfLinkModel = None + import sys + print("qpageview: " + "PDF links are disabled because QPdfLinkModel is unavailable.", + file=sys.stderr) from . import document from . import page from . import link from . import locking from . import render - - -# store the links in the page of a document as long as the document exists -_linkscache = weakref.WeakKeyDictionary() +from . import util class Link(link.Link): """A link that encapsulates QPdfLinkModel data.""" def __init__(self, linkobj, index, pointSize): - self.linkobj = linkobj - self.index = index + self._targetPage = linkobj.data(index, QPdfLinkModel.Role.Page.value) + self._url = linkobj.data(index, + QPdfLinkModel.Role.Url.value).toString() # Convert to relative coordinates between 0.0 and 1.0 as expected # by link.Link, which uses them for compatibility with Poppler rect = linkobj.data(index, QPdfLinkModel.Role.Rectangle.value) @@ -70,14 +79,12 @@ """If this is an internal link, the page number to which the link should jump; otherwise -1.""" # QtPdf pages are 0-indexed, but our View is 1-indexed - page = self.linkobj.data(self.index, QPdfLinkModel.Role.Page.value) - return (page + 1) if page != -1 else -1 + return -1 if self._targetPage == -1 else self._targetPage + 1 @property def url(self): """The URL the link points to.""" - url = self.linkobj.data(self.index, - QPdfLinkModel.Role.Url.value).toString() + url = self._url if platform.system() == "Windows": # Fix weird things QUrl does to local paths for proto in ("file", "textedit"): @@ -153,7 +160,7 @@ def text(self, rect): """Returns text inside rectangle.""" - rectf = self.mapFromPage(self.pageWidth, self.pageHeight).rect(rect) + rectf = self.mapFromPage().rect(rect) with locking.lock(self.document): return self.document.getSelection( self.pageNumber, rectf.topLeft(), rectf.bottomRight()).text() @@ -162,8 +169,12 @@ """Return links inside the document.""" document, pageNumber = self.document, self.pageNumber try: - return _linkscache[document][pageNumber] - except KeyError: + return self._linksCache + except AttributeError: + if QPdfLinkModel is None: + # Link support is unavailable; return an empty cache + self._linksCache = link.Links() + return self._linksCache with locking.lock(document): lm = QPdfLinkModel(document=document, page=pageNumber) parentIndex = QModelIndex() @@ -172,9 +183,8 @@ index = lm.index(row, 0, parentIndex) links.append(Link(lm, index, document.pagePointSize(pageNumber))) - links = link.Links(links) - _linkscache.setdefault(document, {})[pageNumber] = links - return links + self._linksCache = link.Links(links) + return self._linksCache class PdfDocument(document.SingleSourceDocument): @@ -210,8 +220,6 @@ 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. @@ -222,12 +230,32 @@ """ 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) @@ -238,39 +266,40 @@ doc = page.document num = page.pageNumber - xres = painter.device().logicalDpiX() - yres = painter.device().logicalDpiY() # 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, # the painter coordinates are scaled to the device's resolution and # scaling is the device's responsibility. - vscale = matrix.m11() - hscale = matrix.m22() - actualSize = (vscale == hscale == 1) + # Note: m11 and m22 are our horizontal and vertical scaling factors. + actualSize = (matrix.m11() == matrix.m22() == 1) # 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 - xresEffective = 72.0 * key.width / pageSize.width() - yresEffective = 72.0 * key.height / pageSize.height() - xMultiplier = 2 if xresEffective < self.oversampleThreshold else 1 - yMultiplier = 2 if yresEffective < self.oversampleThreshold else 1 - else: - xMultiplier = 1 - yMultiplier = 1 + multiplier = util.oversampleFactor(key, pageSize) if actualSize else 1 + + # Set rendering options + RenderFlag = QPdfDocumentRenderOptions.RenderFlag + renderOptions = QPdfDocumentRenderOptions() + # This ridiculous back-and-forth conversion is necessary because + # PyQt6's enum types don't implement bitwise OR + renderFlags = 0 + if not self.antialiasing: + renderFlags |= RenderFlag.TextAliased.value + renderFlags |= RenderFlag.ImageAliased.value + renderFlags |= RenderFlag.PathAliased.value + renderOptions.setRenderFlags(RenderFlag(renderFlags)) # Render the image at the output device's resolution (or double # that if we are oversampling) - s = matrix.scale(xMultiplier, yMultiplier).mapRect(source) - image = self._render_image(doc, num, - xres * xMultiplier, yres * yMultiplier, - int(s.width()), int(s.height()), paperColor) + 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) if tile != (0, 0, key.width, key.height): # Crop the image to the tile boundaries @@ -282,45 +311,7 @@ Qt.AspectRatioMode.IgnoreAspectRatio, Qt.TransformationMode.SmoothTransformation) - # Erase the target area and draw the image - painter.eraseRect(target) - painter.drawImage(target, image, QRectF(image.rect())) - - def _render_image(self, doc, pageNum, - xres=72.0, yres=72.0, w=-1, h=-1, paperColor=None): - """Render an image. - - This always renders the full page because that is the only rendering - mode supported by QtPdf. If you need a smaller area, you can crop - the returned QImage by calling its copy(x, y, w, h) method. - - The document is properly locked during rendering and render options - are set. - - """ - RenderFlag = QPdfDocumentRenderOptions.RenderFlag - with locking.lock(doc): - options = QPdfDocumentRenderOptions() - - # This ridiculous back-and-forth conversion is necessary because - # PyQt6 won't let you just 'OR' together RenderFlag constants. - renderFlags = 0 - if not self.antialiasing: - renderFlags |= RenderFlag.TextAliased.value - renderFlags |= RenderFlag.ImageAliased.value - renderFlags |= RenderFlag.PathAliased.value - options.setRenderFlags(RenderFlag(renderFlags)) - - image = doc.render(pageNum, QSize(int(w), int(h)), options) - if paperColor: - # QtPdf leaves the page background transparent, so we need to - # paint it ourselves. - content = image.copy() - painter = QPainter(image) - painter.fillRect(image.rect(), paperColor) - painter.drawImage(0, 0, content) - painter.end() - return image + return image def load(source): @@ -337,7 +328,9 @@ if isinstance(source, QPdfDocument): return source elif isinstance(source, str) or isinstance(source, QByteArray): - document = QPdfDocument(QCoreApplication.instance()) + # We need to create the QPdfDocument without a parent QObject so + # Python can garbage-collect it properly when it goes out of scope + document = QPdfDocument(None) # parent has no default value document.load(source) return document diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qpageview-1.0.3/qpageview/pkginfo.py new/qpageview-1.0.5/qpageview/pkginfo.py --- old/qpageview-1.0.3/qpageview/pkginfo.py 2020-02-02 01:00:00.000000000 +0100 +++ new/qpageview-1.0.5/qpageview/pkginfo.py 2020-02-02 01:00:00.000000000 +0100 @@ -21,4 +21,4 @@ """Version information on the qpageview package.""" -version_string = "1.0.3" +version_string = "1.0.5" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qpageview-1.0.3/qpageview/render.py new/qpageview-1.0.5/qpageview/render.py --- old/qpageview-1.0.3/qpageview/render.py 2020-02-02 01:00:00.000000000 +0100 +++ new/qpageview-1.0.5/qpageview/render.py 2020-02-02 01:00:00.000000000 +0100 @@ -219,17 +219,15 @@ 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 +369,8 @@ 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 +380,7 @@ pending job. """ + self.cache.prepare(tiles) for tile in tiles: try: job = _jobs[(key, tile)] @@ -398,7 +399,9 @@ 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() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qpageview-1.0.3/qpageview/util.py new/qpageview-1.0.5/qpageview/util.py --- old/qpageview-1.0.3/qpageview/util.py 2020-02-02 01:00:00.000000000 +0100 +++ new/qpageview-1.0.5/qpageview/util.py 2020-02-02 01:00:00.000000000 +0100 @@ -304,6 +304,31 @@ 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
