-
-
Save ssokolow/7e9784f45314b9c70e436d788cd1c65e to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3 | |
# -*- coding: utf-8 -*- | |
"""Example code for a PyQt image-display widget which Just Works™ | |
TODO: Split this into a loader wrapper and a widget wrapper so it can be used | |
in designs which maintain a preloaded queue of upcoming images to improve | |
the perception of quick load times. | |
""" | |
from __future__ import (absolute_import, division, print_function, | |
with_statement, unicode_literals) | |
__author__ = "Stephan Sokolow (deitarion/SSokolow)" | |
__license__ = "MIT" | |
from PyQt5.QtCore import QSize, Qt | |
from PyQt5.QtGui import QImageReader, QMovie, QPalette, QPixmap | |
from PyQt5.QtWidgets import QApplication, QFrame, QLabel, QVBoxLayout | |
class SaneDefaultsImageLabel(QFrame): | |
"""Compound widget to work around some shortcomings in Qt image display. | |
- Animated GIFs will animate, like in a browser, by transparently switching | |
between QImage and QMovie internally depending on the number of frames | |
detected by QImageReader. | |
- Content will scale up or down to fit the widget while preserving its | |
aspect ratio and will do so without imposing a minimum size of 100%. | |
- Letterbox/pillarbox borders will default to black. | |
(It's a bit of a toss-up whether an application will want this or the | |
default window background colour, so this defaults to the choice that | |
provides an example of how to accomplish it.) | |
Note that QImageReader doesn't have an equivalent to GdkPixbufLoader's | |
`area-prepared` and `area-updated` signals, so incremental display for | |
for high-speed scanning (ie. hitting "next" based on a partially loaded | |
images) isn't really possible. The closest one can get is to experiment | |
with QImageReader's support for loading just part of a JPEG file to see if | |
it can be done without significantly adding to the whole-image load time. | |
(https://wiki.qt.io/Loading_Large_Images) | |
""" | |
movie_aspect = None | |
orig_pixmap = None | |
def __init__(self): | |
super(SaneDefaultsImageLabel, self).__init__() | |
# We need a layout if we want to prevent the image from distorting | |
layout = QVBoxLayout() | |
self.setLayout(layout) | |
self.label = QLabel() | |
self.label.setAlignment(Qt.AlignCenter) | |
layout.addWidget(self.label) | |
# Set the letterbox/pillarbox bars to be black | |
# https://wiki.qt.io/How_to_Change_the_Background_Color_of_QWidget | |
pal = self.palette() | |
pal.setColor(QPalette.Background, Qt.black) | |
self.setAutoFillBackground(True) | |
self.setPalette(pal) | |
# No black bordering on non-letterbox/pillarbox edges | |
layout.setContentsMargins(0, 0, 0, 0) | |
def load(self, source): | |
"""Load anything that QImageReader or QMovie constructors accept""" | |
# Use QImageReader to identify animated GIFs for separate handling | |
# (Thanks to https://stackoverflow.com/a/20674469/435253 for this) | |
image_reader = QImageReader(source) | |
from PyQt5.QtGui import QImageIOHandler | |
if image_reader.supportsAnimation() and image_reader.imageCount() > 1: | |
movie = QMovie(source) | |
# Calculate the aspect ratio and adjust the widget size | |
movie.jumpToFrame(0) | |
movie_size = movie.currentImage().size() | |
self.movie_aspect = movie_size.width() / movie_size.height() | |
self.resizeEvent() | |
self.label.setMovie(movie) | |
movie.start() | |
# Free memory if the previous image was non-animated | |
self.orig_pixmap = None | |
else: | |
self.orig_pixmap = QPixmap(image_reader.read()) | |
self.label.setPixmap(self.orig_pixmap) | |
# Fail quickly if our violated invariants result in stale | |
# aspect-ratio information getting reused | |
self.movie_aspect = None | |
# Keep the image from preventing downscaling | |
self.setMinimumSize(1, 1) | |
def resizeEvent(self, _event=None): | |
"""Resize handler to update dimensions of displayed image/animation""" | |
rect = self.geometry() | |
movie = self.label.movie() | |
if movie: | |
# Manually implement aspect-preserving scaling for QMovie | |
# | |
# Thanks to Spencer @ https://stackoverflow.com/a/50166220/435253 | |
# for figuring out that this approach must be taken to get smooth | |
# up-scaling out of QMovie. | |
width = rect.height() * self.movie_aspect | |
if width <= rect.width(): | |
size = QSize(width, rect.height()) | |
else: | |
height = rect.width() / self.movie_aspect | |
size = QSize(rect.width(), height) | |
movie.setScaledSize(size) | |
elif self.orig_pixmap and not self.orig_pixmap.isNull(): | |
# To avoid having to change which widgets are hidden and shown, | |
# do our upscaling manually. | |
# | |
# This probably won't be suitable for widgets intended to be | |
# resized as part of normal operation (aside from initially, when | |
# the window appears) but it works well enough for my use cases and | |
# was the quickest, simplest thing to implement. | |
# | |
# If your problem is downscaling very large images, I'd start by | |
# making this one- or two-line change to see if it's good enough: | |
# 1. Use Qt.FastTransformation to scale to the closest power of | |
# two (eg. 1/2, 1/4, 1/8, etc.) that's still bigger and gives a | |
# decent looking intermediate result. | |
# 2. Use Qt.SmoothTransform to take the final step to the desired | |
# size. | |
# | |
# If it's not or you need actual animation, you'll want to look up | |
# how to do aspect-preserving display of images and animations | |
# under QML (embeddable in a QWidget GUI using QQuickWidget) so Qt | |
# can offload the scaling to the GPU. | |
size = QSize(rect.width(), rect.height()) | |
# Don't waste CPU generating a new pixmap if the resize didn't | |
# alter the dimension that's currently bounding its size | |
pixmap_size = self.label.pixmap().size() | |
if (pixmap_size.width() == size.width() and | |
pixmap_size.height() <= size.height()): | |
return | |
if (pixmap_size.height() == size.height() and | |
pixmap_size.width() <= size.width()): | |
return | |
self.label.setPixmap(self.orig_pixmap.scaled(size, | |
Qt.KeepAspectRatio, Qt.SmoothTransformation)) | |
def main(): | |
"""Main entry point for demonstration code""" | |
import sys | |
if len(sys.argv) != 2: | |
print("Usage: {} <image path>".format(sys.argv[0])) | |
sys.exit(1) | |
# I don't know how reliable it is, but making `app` a global which outlives | |
# `main()` seems to fix the "segmentation fault on exit" bug caused by | |
# Python and Qt disagreeing on the destruction order for the QObject tree | |
# and it's certainly the most concise solution I've yet found. | |
global app # pylint: disable=global-statement, global-variable-undefined | |
app = QApplication(sys.argv) | |
# Take advantage of how any QWidget subclass can be used as a top-level | |
# window for demonstration purposes | |
window = SaneDefaultsImageLabel() | |
window.load(sys.argv[1]) | |
window.show() | |
sys.exit(app.exec_()) | |
if __name__ == "__main__": | |
main() | |
# vim: set sw=4 sts=4 expandtab : |
I'm honestly surprised that they don't provide something like this as standard. It feels like forgetting to provide a multi-line text field as standard and requiring users to build one by monkey-patching the single-line variety.
I'm honestly surprised that they don't provide something like this as standard. It feels like forgetting to provide a multi-line text field as standard and requiring users to build one by monkey-patching the single-line variety.
Me too. I have been going through the official library for days just because I can't believe there isn't one.
BTW, I noticed there are some minor bugs when handling loading and it lacks an implementation of adjustSize(), which set the widget size to fit the original size of the content.
I may do some modifications and let you decide if you need to merge.
I did the following:
1) re-arrange the structure to make things clearer.
2) Fix wrong image size while re-loading image/ Fix GIF flicker while loading
3) add adjustSize(), which performs like QLabel.adjustSize()
check my fork https://gist.github.com/yku12cn/afa2db2ff19202bfba080dff9a166c44
wow! really good work!!!
Well done..
I have been searching for a while for something like this.. exactly what I need.