From 145de3311cb9f32ac0bc9842b0600fcad84fed16 Mon Sep 17 00:00:00 2001 From: Tim Keller Date: Fri, 9 May 2025 21:58:54 -0500 Subject: add support for downloading new images from immich api on a separate thread --- immich.py | 33 ++++++++++++++++++ pix.py | 97 ++++++++------------------------------------------- texture.py | 43 +++++++++++++++++++++++ transition.py | 12 +++---- window.py | 110 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 206 insertions(+), 89 deletions(-) create mode 100644 immich.py create mode 100644 texture.py create mode 100644 window.py diff --git a/immich.py b/immich.py new file mode 100644 index 0000000..1f1ccb8 --- /dev/null +++ b/immich.py @@ -0,0 +1,33 @@ +import requests +from io import BytesIO + +from texture import ImageTexture + + +class ImmichConnector: + def __init__(self, server_url, api_key): + self.server_url = server_url.removesuffix("/") + self.api_key = api_key + + def _request(self, endpoint): + return requests.get(f"{self.server_url}/api/{endpoint}", headers={ "x-api-key": self.api_key }) + + def load_album_assets(self, key): + response = self._request(f"albums/{key}") + if response.status_code != 200: return + + data = response.json() + return data["assets"] + + def load_image(self, pd, key, exif=None): + response = self._request(f"assets/{key}/thumbnail?size=fullsize") + if response.status_code != 200: return + + image_data = BytesIO(response.content) + it = ImageTexture(image_data, exif) + pd.textures.append(it) + print(f"Loaded image {key}") + + def idle(self, pd): + for asset in self.load_album_assets("bac029a5-972b-4519-bce0-a0d74add3969"): + self.load_image(pd, asset["id"], asset["exifInfo"]) diff --git a/pix.py b/pix.py index 988f83d..41d28d6 100644 --- a/pix.py +++ b/pix.py @@ -1,89 +1,20 @@ import sys -from OpenGL.GL import * -from OpenGL.GLUT import * -from OpenGL.GLU import * -from PIL import Image -from time import time +from threading import Thread -from transition import Transition - -display_time = 2.0 -transition_duration = 0.5 - -# Load image as a texture using PIL -def load_texture(image_path): - img = Image.open(image_path) - img = img.convert("RGBA") # Ensure the image is in RGBA mode (4 channels: R, G, B, A) - img_data = img.tobytes("raw", "RGBA", 0, -1) # Convert image data to bytes - - texture_id = glGenTextures(1) - glBindTexture(GL_TEXTURE_2D, texture_id) - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, img.width, img.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, img_data) - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) - return texture_id - -# Main display function -transition = Transition() -def display(): - global start_time, image_time, last_time, alpha, texture_id1, texture_id2 - - current_time = time() - alive_time = current_time - start_time - delta_time = current_time - last_time - last_time = current_time - image_time += delta_time - - if (image_time < display_time): - return - - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) - glLoadIdentity() - - # Get window size - window_width, window_height = glutGet(GLUT_WINDOW_WIDTH), glutGet(GLUT_WINDOW_HEIGHT) - - # DRAW - transition_time = image_time - display_time - complete = transition.draw(texture_id1, texture_id2, window_width, window_height, delta_time, transition_time, transition_duration) - - glClearColor(0.0, 0.0, 0.0, 1.0) # Set the background color to black - - glutSwapBuffers() - - if (complete): - image_time = 0 - texture_id1, texture_id2 = texture_id2, texture_id1 - -# Initialization and main loop -def main(): - global texture_id1, texture_id2, alpha, last_time, start_time, image_time - - # Initialize the window - glutInit(sys.argv) - glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB) - glutCreateWindow("Image Viewer with Fade Transition") - glEnable(GL_TEXTURE_2D) +from window import PixDisplay +from immich import ImmichConnector +def load_textures(pd): # Load two images for transition - texture_id1 = load_texture("image1.jpg") - texture_id2 = load_texture("image2.jpg") - - alpha = 0.0 # Start with fully transparent - image_time = 0 - start_time = time() - last_time = time() - - # Set up the OpenGL viewport and projection - glMatrixMode(GL_PROJECTION) - glLoadIdentity() - glOrtho(-1, 1, -1, 1, -1, 1) - glMatrixMode(GL_MODELVIEW) - - glutDisplayFunc(display) - glutIdleFunc(display) - glutMainLoop() + tex = ImageTexture("image1.jpg") + pd.textures.append(tex) + tex = ImageTexture("image2.jpg") + pd.textures.append(tex) if __name__ == "__main__": - main() - + immichConnector = ImmichConnector("http://192.168.1.13", "m5nqOoBc4uhAba21gZdCP3z8D3JT4GPxDXL2psd52EA") + pd = PixDisplay() + #t1 = Thread(target=load_textures, daemon=True, args=(pd,)) + t1 = Thread(target=immichConnector.idle, daemon=True, args=(pd,)) + t1.start() + pd.main() diff --git a/texture.py b/texture.py new file mode 100644 index 0000000..cf37d6a --- /dev/null +++ b/texture.py @@ -0,0 +1,43 @@ +from OpenGL.GL import * +from OpenGL.GLUT import * +from OpenGL.GLU import * +from PIL import Image, ExifTags + +class ImageTexture: + def __init__(self, image_source, exif=None): + self.id = None + img = Image.open(image_source) + self.exif = exif or ImageTexture.get_exif(img) + img = ImageTexture.handle_orientation(img, exif) + img = img.convert("RGBA") # Ensure the image is in RGBA mode + self.width = img.width + self.height = img.height + self._img_data = img.tobytes("raw", "RGBA", 0, -1) # Convert image data to bytes + + def gl_init(self): + if self.id: + return + #glPixelStorei(GL_UNPACK_ALIGNMENT, 1) + self.id = glGenTextures(1) + glBindTexture(GL_TEXTURE_2D, self.id) + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, self.width, self.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, self._img_data) + #glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, img.width, img.height, 0, GL_RGB, GL_UNSIGNED_BYTE, img_data) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) + del self._img_data # No longer needed, clear mem + + print(f"Loaded texture {self.id}") + + @staticmethod + def get_exif(img): + return { ExifTags.TAGS[k]: v for k, v in img.getexif().items() if k in ExifTags } + + @staticmethod + def handle_orientation(img, exif): + orientation = exif.get("Orientation", 1) + + if orientation == 3: return img.rotate(180, expand=True) + if orientation == 6: return img.rotate(270, expand=True) + if orientation == 8: return img.rotate( 90, expand=True) + + return img diff --git a/transition.py b/transition.py index cf2acb4..a076889 100644 --- a/transition.py +++ b/transition.py @@ -10,25 +10,25 @@ class Transition: alpha = 1.0 # Draw the first image - self._draw_image(texture_prev, 3840, 2160, window_width, window_height, 1 - alpha) # TODO instead of decreasing alpha, draw transparent letterboxes + self.draw_image(texture_prev, window_width, window_height, 1 - alpha) # TODO instead of decreasing alpha, draw transparent letterboxes # Draw the second image (with transparency) - self._draw_image(texture_next, 3840, 2160, window_width, window_height, alpha) + self.draw_image(texture_next, window_width, window_height, alpha) return alpha >= 1.0 # Complete # Draw the image with blending enabled (to allow fade effect) - def _draw_image(self, texture_id, img_width, img_height, window_width, window_height, alpha): - if (not alpha): return + def draw_image(self, texture, window_width, window_height, alpha): + if not alpha: return glEnable(GL_BLEND) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) - glBindTexture(GL_TEXTURE_2D, texture_id) + glBindTexture(GL_TEXTURE_2D, texture.id) glColor4f(1.0, 1.0, 1.0, alpha) # Set alpha to control transparency # Calculate aspect ratio - img_aspect = img_width / float(img_height) + img_aspect = texture.width / float(texture.height) win_aspect = window_width / float(window_height) scaled_width = window_width diff --git a/window.py b/window.py new file mode 100644 index 0000000..29d0522 --- /dev/null +++ b/window.py @@ -0,0 +1,110 @@ +from OpenGL.GL import * +from OpenGL.GLUT import * +from OpenGL.GLU import * +from time import time + +from transition import Transition +from texture import ImageTexture + + +class PixDisplay: + def __init__(self): + self.last_time = 0 + self.start_time = 0 + self.image_time = 0 + self.textures = [] + self.current_texture_index = 0 + self.transition = Transition() + self.window_width = 0 + self.window_height = 0 + + self.display_duration = 2.0 + self.transition_duration = 0.5 + + @property + def next_texture_index(self): return (self.current_texture_index + 1) % len(self.textures) + + @property + def texture_current(self): return self.textures[self.current_texture_index] + + @property + def texture_next(self): return self.textures[self.next_texture_index] + + # Main display function + def display(self): + # Calculate timings + current_time = time() + alive_time = current_time - self.start_time + delta_time = current_time - self.last_time + self.last_time = current_time + + if not self.textures: + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) + glLoadIdentity() + glClearColor(0.0, 0.0, 0.0, 1.0) # Set the background color to black + + glutSwapBuffers() + return + + # Ensure textures are initialized + self.texture_current.gl_init() + self.texture_next.gl_init() + + # Progress image time + self.image_time += delta_time + + # Get window size + window_width, window_height = glutGet(GLUT_WINDOW_WIDTH), glutGet(GLUT_WINDOW_HEIGHT) + + if self.image_time < self.display_duration: + if window_width != self.window_width or window_height != self.window_height: + self.window_width, self.window_height = window_width, window_height + + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) + glLoadIdentity() + + self.transition.draw_image(self.texture_current, self.window_width, self.window_height, 1) + + glClearColor(0.0, 0.0, 0.0, 1.0) # Set the background color to black + glutSwapBuffers() + return + + self.window_width, self.window_height = window_width, window_height + + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) + glLoadIdentity() + + # DRAW + transition_time = self.image_time - self.display_duration + complete = self.transition.draw(self.texture_current, self.texture_next, self.window_width, self.window_height, delta_time, transition_time, self.transition_duration) + + glClearColor(0.0, 0.0, 0.0, 1.0) # Set the background color to black + + glutSwapBuffers() + + if complete: + self.image_time = 0 + self.current_texture_index = self.next_texture_index + + # Initialization and main loop + def main(self): + # Initialize the window + glutInit(sys.argv) + glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB) + glutCreateWindow("Image Viewer with Fade Transition") + glEnable(GL_TEXTURE_2D) + + self.image_time = 0 + self.start_time = time() + self.last_time = time() + + # Set up the OpenGL viewport and projection + glMatrixMode(GL_PROJECTION) + glLoadIdentity() + glOrtho(-1, 1, -1, 1, -1, 1) + glMatrixMode(GL_MODELVIEW) + + glutDisplayFunc(self.display) + glutIdleFunc(self.display) + glutMainLoop() + -- cgit v1.2.3