import gc import os from mpos import Activity, WidgetAnimator, DisplayMetrics class ImageView(Activity): imagedir = "data/images" images = [] image_nr = None image_timer = None fullscreen = False stopping = False # Widgets image = None gif = None current_image_dsc = None # Track current image descriptor def onCreate(self): screen = lv.obj() screen.remove_flag(lv.obj.FLAG.SCROLLABLE) self.image = lv.image(screen) self.image.center() self.image.add_flag(lv.obj.FLAG.CLICKABLE) self.image.add_event_cb(lambda e: self.toggle_fullscreen(),lv.EVENT.CLICKED,None) self.gif = lv.gif(screen) self.gif.center() self.gif.add_flag(lv.obj.FLAG.CLICKABLE) self.gif.add_flag(lv.obj.FLAG.HIDDEN) self.gif.add_event_cb(lambda e: self.toggle_fullscreen(),lv.EVENT.CLICKED,None) self.label = lv.label(screen) self.label.set_text(f"Loading images from\n{self.imagedir}") self.label.align(lv.ALIGN.TOP_MID,0,0) self.label.set_width(lv.pct(80)) self.prev_button = lv.button(screen) self.prev_button.align(lv.ALIGN.BOTTOM_LEFT,0,0) self.prev_button.add_event_cb(lambda e: self.show_prev_image_if_fullscreen(),lv.EVENT.FOCUSED,None) self.prev_button.add_event_cb(lambda e: self.show_prev_image(),lv.EVENT.CLICKED,None) prev_label = lv.label(self.prev_button) prev_label.set_text(lv.SYMBOL.LEFT) prev_label.set_style_text_font(lv.font_montserrat_16, lv.PART.MAIN) self.play_button = lv.button(screen) self.play_button.align(lv.ALIGN.BOTTOM_MID,0,0) self.play_button.set_style_opa(lv.OPA.TRANSP, lv.PART.MAIN) #self.play_button.add_flag(lv.obj.FLAG.HIDDEN) #self.play_button.add_event_cb(lambda e: self.unfocus_if_not_fullscreen(),lv.EVENT.FOCUSED,None) #self.play_button.set_style_shadow_opa(lv.OPA.TRANSP, lv.PART.MAIN) #self.play_button.add_event_cb(lambda e: self.play(),lv.EVENT.CLICKED,None) #play_label = lv.label(self.play_button) #play_label.set_text(lv.SYMBOL.PLAY) self.delete_button = lv.button(screen) self.delete_button.align(lv.ALIGN.BOTTOM_MID,0,0) self.delete_button.add_event_cb(lambda e: self.delete_image(),lv.EVENT.CLICKED,None) delete_label = lv.label(self.delete_button) delete_label.set_text(lv.SYMBOL.TRASH) delete_label.set_style_text_font(lv.font_montserrat_16, lv.PART.MAIN) self.next_button = lv.button(screen) self.next_button.align(lv.ALIGN.BOTTOM_RIGHT,0,0) #self.next_button.add_event_cb(self.print_events, lv.EVENT.ALL, None) self.next_button.add_event_cb(lambda e: self.show_next_image_if_fullscreen(),lv.EVENT.FOCUSED,None) self.next_button.add_event_cb(lambda e: self.show_next_image(),lv.EVENT.CLICKED,None) next_label = lv.label(self.next_button) next_label.set_text(lv.SYMBOL.RIGHT) next_label.set_style_text_font(lv.font_montserrat_16, lv.PART.MAIN) #screen.add_event_cb(self.print_events, lv.EVENT.ALL, None) self.setContentView(screen) def onResume(self, screen): self.stopping = False self.images.clear() try: for item in os.listdir(self.imagedir): print(item) lowercase = item.lower() if not (lowercase.endswith(".jpg") or lowercase.endswith(".jpeg") or lowercase.endswith(".png") or lowercase.endswith(".raw") or lowercase.endswith(".gif")): continue fullname = f"{self.imagedir}/{item}" size = os.stat(fullname)[6] print(f"size: {size}") if size > 10 * 1024*1024: print(f"Skipping file of size {size}") continue self.images.append(fullname) self.images.sort() if len(self.images) == 0: self.no_image_mode() else: # Begin with one image: self.show_next_image() self.stop_fullscreen() except Exception as e: print(f"ImageView encountered exception for {self.imagedir}: {e}") def onStop(self, screen): print("ImageView stopping") self.stopping = True if self.image_timer: print("ImageView: deleting image_timer") self.image_timer.delete() def no_image_mode(self): self.label.set_text(f"No images found in {self.imagedir}...") WidgetAnimator.smooth_hide(self.prev_button) WidgetAnimator.smooth_hide(self.delete_button) WidgetAnimator.smooth_hide(self.next_button) def show_prev_image(self, event=None): print("showing previous image...") if len(self.images) < 1: self.no_image_mode() return if self.image_nr is None or self.image_nr == 0: self.image_nr = len(self.images) - 1 else: self.image_nr = self.image_nr - 1 name = self.images[self.image_nr] print(f"show_prev_image showing {name}") self.show_image(name) def toggle_fullscreen(self, event=None): print("playing...") if self.fullscreen: self.fullscreen = False self.stop_fullscreen() else: self.fullscreen = True self.start_fullscreen() self.scale_image() def stop_fullscreen(self): print("stopping fullscreen") WidgetAnimator.smooth_show(self.label) WidgetAnimator.smooth_show(self.prev_button) WidgetAnimator.smooth_show(self.delete_button) #WidgetAnimator.smooth_show(self.play_button) self.play_button.add_flag(lv.obj.FLAG.HIDDEN) # make it not accepting focus WidgetAnimator.smooth_show(self.next_button) def start_fullscreen(self): print("starting fullscreen") WidgetAnimator.smooth_hide(self.label) WidgetAnimator.smooth_hide(self.prev_button, hide=False) WidgetAnimator.smooth_hide(self.delete_button, hide=False) #WidgetAnimator.smooth_hide(self.play_button, hide=False) self.play_button.remove_flag(lv.obj.FLAG.HIDDEN) # make it accepting focus WidgetAnimator.smooth_hide(self.next_button, hide=False) self.unfocus() # focus on the invisible center button, not previous or next def show_prev_image_if_fullscreen(self, event=None): if self.stopping: # closing the window results in a focus shift, which can trigger the next action in fullscreen return if self.fullscreen: self.unfocus() self.show_prev_image() def show_next_image_if_fullscreen(self, event=None): if self.stopping: # closing the window results in a focus shift, which can trigger the next action in fullscreen return if self.fullscreen: self.unfocus() self.show_next_image() def unfocus(self): focusgroup = lv.group_get_default() if not focusgroup: print("WARNING: imageview.py could not get default focus group") return print("got focus group") # group.focus_obj(self.play_button) would be better but appears missing?! focused = focusgroup.get_focused() print("got focus button") #focused.remove_state(lv.STATE.FOCUSED) # this doesn't seem to work to remove focus if focused: print("checking which button is focused") if focused == self.next_button: print("next is focused") focusgroup.focus_prev() elif focused == self.prev_button: print("prev is focused") focusgroup.focus_next() else: print("focus isn't on next or previous, leaving it...") def show_next_image(self, event=None): print("showing next image...") if len(self.images) < 1: self.no_image_mode() return if self.image_nr is None or self.image_nr >= len(self.images) - 1: self.image_nr = 0 else: self.image_nr = self.image_nr + 1 name = self.images[self.image_nr] print(f"show_next_image showing {name}") self.show_image(name) def delete_image(self, event=None): filename = self.images[self.image_nr] try: os.remove(filename) self.clear_image() self.label.set_text(f"Deleted\n{filename}") del self.images[self.image_nr] except Exception as e: print(f"Error deleting {filename}: {e}") def extract_dimensions_and_format(self, filename): # Split the filename by '_' parts = filename.split('_') # Get the color format (last part before '.raw') color_format = parts[-1].split('.')[0] # e.g., "RGB565" # Get the resolution (second-to-last part) resolution = parts[-2] # e.g., "240x240" # Split resolution by 'x' to get width and height width, height = map(int, resolution.split('x')) return width, height, color_format.upper() def show_image(self, name): self.current_image = name try: self.label.set_text(name) self.clear_image() if name.lower().endswith(".gif"): print("switching to gif mode...") self.image.add_flag(lv.obj.FLAG.HIDDEN) self.gif.remove_flag(lv.obj.FLAG.HIDDEN) self.gif.set_src(f"M:{name}") else: self.gif.add_flag(lv.obj.FLAG.HIDDEN) self.image.remove_flag(lv.obj.FLAG.HIDDEN) self.image.set_src(f"M:{name}") if name.lower().endswith(".raw"): f = open(name, 'rb') image_data = f.read() print(f"loaded {len(image_data)} bytes from .raw file") f.close() try: width, height, color_format = self.extract_dimensions_and_format(name) except ValueError as e: print(f"Warning: could not extract dimensions and format from raw image: {e}") return print(f"Raw image has width: {width}, Height: {height}, Color Format: {color_format}") stride = width * 2 cf = lv.COLOR_FORMAT.RGB565 if color_format == "GRAY": cf = lv.COLOR_FORMAT.L8 stride = width elif color_format != "RGB565": print(f"WARNING: unknown color format {color_format}, assuming RGB565...") self.current_image_dsc = lv.image_dsc_t({ "header": { "magic": lv.IMAGE_HEADER_MAGIC, "w": width, "h": height, "stride": stride, "cf": cf }, 'data_size': len(image_data), 'data': image_data }) self.image.set_src(self.current_image_dsc) self.scale_image() except OSError as e: print(f"show_image got exception: {e}") def scale_image(self): if self.fullscreen: pct = 100 else: pct = 70 lvgl_w = DisplayMetrics.pct_of_width(pct) lvgl_h = DisplayMetrics.pct_of_height(pct) print(f"scaling to size: {lvgl_w}x{lvgl_h}") header = lv.image_header_t() self.image.decoder_get_info(self.image.get_src(), header) image_w = header.w image_h = header.h if image_w == 0 or image_h == 0: return print(f"the real image has size: {header.w}x{header.h}") scale_factor_w = round(lvgl_w * 256 / image_w) scale_factor_h = round(lvgl_h * 256 / image_h) print(f"scale_factors: {scale_factor_w},{scale_factor_h}") self.image.set_size(lvgl_w, lvgl_h) #self.gif.set_size(lvgl_w, lvgl_h) doesn't seem to do anything. get_style_transform_scale_x/y works but then it needs get_style_translate_x/y #self.image.set_scale(max(scale_factor_w,scale_factor_h)) # fills the entire screen but cuts off borders self.image.set_scale(min(scale_factor_w,scale_factor_h)) print(f"after set_scale, the LVGL image has size: {self.image.get_width()}x{self.image.get_height()}") def clear_image(self): """Clear current image or GIF source to free memory.""" self.image.set_src(None) #if self.current_image_dsc: # self.current_image_dsc = None # Release reference to descriptor #self.image.set_src(None) # Clear image source #self.gif.set_src(None) # Clear GIF source causes crash! #self.gif.add_flag(lv.obj.FLAG.HIDDEN) #self.image.remove_flag(lv.obj.FLAG.HIDDEN) #lv.image_cache_invalidate_src(None) # Invalidate LVGL image cache # These 2 are needed to enable infinite slides with just 10MB RAM: lv.image.cache_drop(None) # This helps a lot! gc.collect() # Force garbage collection seems to fix memory alloc issues!