class QtReader(BaseReader): """Image reader using Qt's QImageReader implementation under the hood.""" def __init__(self, path: str, file_format: str): super().__init__(path, file_format) self._handler = QImageReader(path, file_format.encode()) self._handler.setAutoTransform(True) if not self._handler.canRead(): # TODO raise ValueError(f"'{path}' cannot be read as image") @classmethod def supports(cls, file_format: str) -> bool: return file_format in QImageReader.supportedImageFormats() @property def is_animation(self) -> bool: return self._handler.supportsAnimation() def get_pixmap(self) -> QPixmap: """Retrieve the pixmap directly from the image reader.""" pixmap = QPixmap.fromImageReader(self._handler) if self._handler.error(): raise ValueError( f"Error reading image '{self.path}': {self._handler.errorString()}" ) return pixmap def get_image(self, size: int) -> QImage: """Retrieve the down-scaled image directly from the image reader.""" qsize = self._handler.size() qsize.scale(size, size, Qt.KeepAspectRatio) self._handler.setScaledSize(qsize) return self._handler.read()
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 __init__(self, file_path, make_opengl_textures=True): image_reader = QImageReader(file_path) self.is_animated = image_reader.supportsAnimation() if self.is_animated: self.num_frames = image_reader.imageCount() # -1 means loop infinitely, 0 means no loop, > 0 is finite # of loops self.loop_count = image_reader.loopCount() self.loops_remaining = 0 self.frames = [] self.delays = [] while image_reader.currentImageNumber() < image_reader.imageCount() - 1: self.frames.append(image_reader.read()) self.delays.append(image_reader.nextImageDelay()) if make_opengl_textures: self.open_gl_textures = [QOpenGLTexture(this_frame.mirrored()) for this_frame in self.frames] self.made_opengl_textures = True self.frames_and_delays = zip(self.frames, self.delays) self.current_frame = 0 self.animating = False else: self.image = image_reader.read() assert isinstance(self.image, QImage) if make_opengl_textures: self.open_gl_texture = QOpenGLTexture(self.image.mirrored()) self.made_opengl_textures = True
def _load(self, path: str, reload_only: bool): """Load proper displayable QWidget for a path. This reads the image using QImageReader and then emits the appropriate *_loaded signal to tell the image to display a new object. """ # Pass file format explicitly as imghdr does a much better job at this than the # file name based approach of QImageReader file_format = files.imghdr.what(path) if file_format is None: log.error("%s is not a valid image", path) return reader = QImageReader(path, file_format.encode("utf-8")) reader.setAutoTransform(True) # Automatically apply exif orientation if not reader.canRead(): log.error("Cannot read image %s", path) return # SVG if file_format == "svg" and QSvgWidget: # Do not store image and only emit with the path as the # VectorGraphic widget needs the path in the constructor self.original = None api.signals.svg_loaded.emit(path, reload_only) self._image_type = ImageType.Svg # Gif elif reader.supportsAnimation(): movie = QMovie(path) if not movie.isValid() or movie.frameCount() == 0: log.error("Error reading animation %s: invalid data", path) return self.original = movie api.signals.movie_loaded.emit(self.current, reload_only) self._image_type = ImageType.Movie # Regular image else: pixmap = QPixmap.fromImageReader(reader) if reader.error(): log.error("Error reading image %s: %s", path, reader.errorString()) return self.original = pixmap api.signals.pixmap_loaded.emit(self.current, reload_only) self._image_type = ImageType.Pixmap self._path = path
def load(self, source, adaptSize=True): """Load anything that QImageReader or QMovie constructors accept adaptSize=True: Initial image size to fit container adaptSize=False: Set container's size the same as image """ # Use QImageReader to identify animated GIFs for separate handling # (Thanks to https://stackoverflow.com/a/20674469/435253 for this) size = QSize(self.width(), self.height()) image_reader = QImageReader(source) if image_reader.supportsAnimation() and image_reader.imageCount() > 1: # Set content as Movie self.content = SaneQMovie(source) # Adjust the widget size if adaptSize: self.content.adaptScale(size) else: # Resizing container will trigger resizeEvent() self.resize(self.content.size()) # Start Movie replay self.setMovie(self.content) self.content.start() else: # Set content as Image self.content = SaneQPixmap(image_reader.read()) # Adjust the widget size if adaptSize: self.setPixmap(self.content.adaptScale(size)) else: # Resizing container will trigger resizeEvent() self.resize(self.content.size()) self.setPixmap(self.content) # Keep the image from preventing downscaling self.setMinimumSize(1, 1)