From fc672bcdff57820d30826159acd5830397c79309 Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Thu, 27 Nov 2025 13:40:43 +0100 Subject: [PATCH] only make textures when GL context exists --- pyPhotoAlbum/models.py | 59 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 48 insertions(+), 11 deletions(-) diff --git a/pyPhotoAlbum/models.py b/pyPhotoAlbum/models.py index 5d57491..f3b0782 100644 --- a/pyPhotoAlbum/models.py +++ b/pyPhotoAlbum/models.py @@ -199,6 +199,11 @@ class ImageData(BaseLayoutElement): # The actual image will be loaded in the background and the texture created # via _on_async_image_loaded() callback when ready + # Create texture from pending image if one exists (deferred from async load) + # This ensures texture creation happens when GL context is active + if hasattr(self, '_pending_pil_image') and self._pending_pil_image is not None: + self._create_texture_from_pending_image() + # Use cached texture if available if hasattr(self, '_texture_id') and self._texture_id: texture_id = self._texture_id @@ -337,12 +342,13 @@ class ImageData(BaseLayoutElement): """ Callback when async image loading completes. + NOTE: This is called from a signal, potentially before GL context is ready. + We store the image and create the texture during the next render() call + when the GL context is guaranteed to be active. + Args: pil_image: Loaded PIL Image (already RGBA, already resized) """ - from OpenGL.GL import (glGenTextures, glBindTexture, glTexImage2D, GL_TEXTURE_2D, - glTexParameteri, GL_TEXTURE_MIN_FILTER, GL_TEXTURE_MAG_FILTER, - GL_LINEAR, GL_RGBA, GL_UNSIGNED_BYTE, glDeleteTextures) from PIL import Image try: @@ -359,6 +365,38 @@ class ImageData(BaseLayoutElement): pil_image = pil_image.transpose(Image.ROTATE_90) # CCW 270 = rotate left print(f"ImageData: Applied PIL rotation {angle}° to {self.image_path}") + # Store the image for texture creation during next render() + # This avoids GL context issues when callback runs on wrong thread/timing + self._pending_pil_image = pil_image + self._img_width = pil_image.width + self._img_height = pil_image.height + self._async_loading = False + + # Update metadata for future renders - always update to reflect rotated dimensions + self.image_dimensions = (pil_image.width, pil_image.height) + + # print(f"ImageData: Image ready for texture creation: {self.image_path}") + + except Exception as e: + print(f"ImageData: Error processing async loaded image: {e}") + self._pending_pil_image = None + self._async_loading = False + + def _create_texture_from_pending_image(self): + """ + Create OpenGL texture from pending PIL image. + Called during render() when GL context is active. + """ + if not hasattr(self, '_pending_pil_image') or self._pending_pil_image is None: + return False + + from OpenGL.GL import (glGenTextures, glBindTexture, glTexImage2D, GL_TEXTURE_2D, + glTexParameteri, GL_TEXTURE_MIN_FILTER, GL_TEXTURE_MAG_FILTER, + GL_LINEAR, GL_RGBA, GL_UNSIGNED_BYTE, glDeleteTextures) + + try: + pil_image = self._pending_pil_image + # Delete old texture if it exists if hasattr(self, '_texture_id') and self._texture_id: glDeleteTextures([self._texture_id]) @@ -376,19 +414,18 @@ class ImageData(BaseLayoutElement): # Cache texture self._texture_id = texture_id self._texture_path = self.image_path - self._img_width = pil_image.width - self._img_height = pil_image.height - self._async_loading = False - # Update metadata for future renders - always update to reflect rotated dimensions - self.image_dimensions = (pil_image.width, pil_image.height) + # Clear pending image to free memory + self._pending_pil_image = None - # print(f"ImageData: Async loaded texture for {self.image_path}") + # print(f"ImageData: Created texture for {self.image_path}") + return True except Exception as e: - print(f"ImageData: Error creating texture from async loaded image: {e}") + print(f"ImageData: Error creating texture: {e}") self._texture_id = None - self._async_loading = False + self._pending_pil_image = None + return False def _on_async_image_load_failed(self, error_msg: str): """