1+ import folder_paths
2+ import os
3+ import torch
4+ import numpy as np
5+ from PIL import Image , ImageDraw , ImageFont
6+ import server # Required for preview generation
7+ import threading
8+ import shutil # For potential file operations
9+ from datetime import datetime # For timestamps
10+
11+ # --- Constants ---
12+ # Determine the base path of the custom node
13+ EXTENSION_PATH = os .path .normpath (os .path .dirname (__file__ ))
14+ DEFAULT_HISTORY_FOLDER = os .path .join (EXTENSION_PATH , "history_folder" )
15+
16+ # Tensor to PIL
17+ def tensor2pil (image ):
18+ if image .dim () > 3 :
19+ image = image [0 ]
20+ return Image .fromarray (np .clip (255. * image .cpu ().numpy ().squeeze (), 0 , 255 ).astype (np .uint8 ))
21+
22+ # Placeholder generation function (creates a base image, will be resized later if needed by UI)
23+ def create_placeholder (size = (128 , 128 ), text = "?" ):
24+ """Creates a simple placeholder PIL image."""
25+ img = Image .new ('RGB' , size , color = (40 , 40 , 40 ))
26+ d = ImageDraw .Draw (img )
27+ font = ImageFont .load_default () # Keep it simple for placeholders
28+ try :
29+ # Basic centering for default font
30+ tw , th = d .textsize (text , font = font ) if hasattr (d , 'textsize' ) else (10 , 10 )
31+ d .text (((size [0 ]- tw )/ 2 , (size [1 ]- th )/ 2 ), text , font = font , fill = (180 , 180 , 180 ))
32+ except Exception as e :
33+ print (f"[PreviewHistory] Error drawing placeholder text: { e } " )
34+ d .text ((10 , 10 ), text , fill = (180 , 180 , 180 )) # Fallback position
35+ return img
36+
37+ class PreviewHistory :
38+ # Lock for file system operations in the target directory
39+ _dir_lock = threading .Lock ()
40+
41+ def __init__ (self ):
42+ self .output_dir = folder_paths .get_temp_directory () # For UI previews
43+ self .type = "temp"
44+
45+ @classmethod
46+ def INPUT_TYPES (s ):
47+ return {
48+ "required" : {
49+ "image" : ("IMAGE" ,),
50+ "history_size" : ("INT" , {
51+ "default" : 9 ,
52+ "min" : 1 ,
53+ "max" : 1000000 ,
54+ "step" : 1 ,
55+ "tooltip" : "How many images to keep in the history folder."
56+ }),
57+ },
58+ }
59+
60+ RETURN_TYPES = ()
61+ FUNCTION = "execute"
62+ OUTPUT_NODE = True
63+ CATEGORY = "Raffle/Previews"
64+
65+ # --- Main Execution ---
66+
67+ def execute (self , image , history_size ):
68+
69+ # Use the default history folder path directly
70+ history_folder = DEFAULT_HISTORY_FOLDER
71+
72+ # Ensure target directory exists
73+ if not os .path .exists (history_folder ): # Use the constant directly
74+ try :
75+ os .makedirs (history_folder , exist_ok = True ) # Use the constant directly
76+ print (f"[PreviewHistory] Created history directory: { history_folder } " ) # Use the constant directly
77+ except OSError as e :
78+ print (f"[PreviewHistory] Error creating directory { history_folder } : { e } . Cannot proceed." ) # Use the constant directly
79+ return {"ui" : {"images" : []}}
80+
81+ # --- Update History Files (if new image provided) ---
82+ if image is not None and image .nelement () > 0 :
83+ new_pil_image = tensor2pil (image )
84+
85+ with PreviewHistory ._dir_lock :
86+ try :
87+ # Save the new image using numerical month timestamp
88+ # Format: history_DD-MM-YYYY_HH-MM-SS.png
89+ now = datetime .now ()
90+ # Format parts: %d=DD, %m=MM(numeric), %Y=YYYY, %H=HH(24h), %M=MM, %S=SS
91+ timestamp_final = now .strftime ("%d-%m-%Y_%H-%M-%S" ) # Use %m for month number
92+
93+ new_filename = f"history_{ timestamp_final } .png"
94+ new_path = os .path .join (history_folder , new_filename ) # Use the constant indirectly via the variable
95+ # print(f"[PreviewHistory] Saving new file: {new_path}") # Debug
96+ new_pil_image .save (new_path , "PNG" , compress_level = 1 )
97+
98+ # Cleanup: Remove oldest files beyond history_size
99+ # 1. Get all .png files with modification times
100+ all_files = []
101+ for filename in os .listdir (history_folder ): # Use the constant indirectly
102+ if filename .lower ().endswith (".png" ):
103+ full_path = os .path .join (history_folder , filename ) # Use the constant indirectly
104+ try :
105+ if os .path .isfile (full_path ): # Ensure it's a file
106+ mod_time = os .path .getmtime (full_path )
107+ all_files .append ((mod_time , full_path ))
108+ except OSError :
109+ print (f"[PreviewHistory] Warning: Could not access file { full_path } during cleanup scan." )
110+ continue # Skip file if cannot get mod_time
111+
112+ # 2. Sort by modification time, newest first
113+ all_files .sort (key = lambda x : x [0 ], reverse = True )
114+
115+ # 3. Remove files exceeding the history size
116+ if len (all_files ) > history_size :
117+ files_to_remove = all_files [history_size :] # Get the oldest ones
118+ # print(f"[PreviewHistory] Found {len(all_files)} files, keeping {history_size}, removing {len(files_to_remove)}.") # Debug
119+ for mod_time , path_to_remove in files_to_remove :
120+ try :
121+ # print(f"[PreviewHistory] Removing old file: {path_to_remove}") # Debug
122+ os .remove (path_to_remove )
123+ except OSError as e :
124+ print (f"[PreviewHistory] Error removing old file { path_to_remove } : { e } " )
125+
126+ except Exception as e :
127+ print (f"[PreviewHistory] Error updating history files in '{ history_folder } ': { e } " ) # Use the constant indirectly
128+ # Attempt to continue to preview whatever state we're in
129+
130+ # --- Load Images for Preview ---
131+ preview_images_pil = []
132+ sorted_history_files = []
133+ with PreviewHistory ._dir_lock : # Use lock for consistency during listing
134+ try :
135+ # Get all .png files with modification times again for loading
136+ temp_files = []
137+ for filename in os .listdir (history_folder ): # Use the constant indirectly
138+ if filename .lower ().endswith (".png" ):
139+ full_path = os .path .join (history_folder , filename ) # Use the constant indirectly
140+ try :
141+ if os .path .isfile (full_path ):
142+ mod_time = os .path .getmtime (full_path )
143+ temp_files .append ((mod_time , full_path ))
144+ except OSError :
145+ # Error getting mod time during load, might be transient, skip file
146+ print (f"[PreviewHistory] Warning: Could not access file { full_path } during load scan." )
147+ continue
148+ # Sort by modification time, newest first
149+ temp_files .sort (key = lambda x : x [0 ], reverse = True )
150+ # Keep only the paths, up to history_size
151+ sorted_history_files = [f [1 ] for f in temp_files [:history_size ]]
152+ except Exception as e :
153+ print (f"[PreviewHistory] Error listing files in { history_folder } for preview: { e } " ) # Use the constant indirectly
154+ # Fall through with empty list
155+
156+ # Determine a consistent size for placeholders if needed
157+ placeholder_size = (128 , 128 ) # Default
158+ if sorted_history_files : # Check if we found any files
159+ try :
160+ # Try loading the first actual image (newest one)
161+ first_image_path = sorted_history_files [0 ]
162+ temp_img = Image .open (first_image_path )
163+ placeholder_size = temp_img .size
164+ temp_img .close ()
165+ except Exception :
166+ pass # Ignore if it fails, keep default
167+
168+ # Load images from the sorted list
169+ for i , fp in enumerate (sorted_history_files ): # Iterate through the paths we collected
170+ try :
171+ img = Image .open (fp ).convert ('RGB' )
172+ preview_images_pil .append (img )
173+ except Exception as e :
174+ print (f"[PreviewHistory] Error loading history image '{ fp } ' for preview: { e } " )
175+ # Keep error placeholder for load failures, use index 'i' for label
176+ preview_images_pil .append (create_placeholder (placeholder_size , f"Err { i :02d} " ))
177+
178+
179+ # --- Generate Preview Data for the UI ---
180+ previews = []
181+ preview_server = server .PromptServer .instance # Get server instance
182+
183+ for i , pil_img in enumerate (preview_images_pil ):
184+ if pil_img is None : continue # Should not happen with placeholder logic
185+
186+ try :
187+ # Use numpy array for saving temporary preview file
188+ img_array = np .array (pil_img ).astype (np .uint8 )
189+ # Define a unique prefix for each temp preview file in the batch
190+ # Simplify prefix - just use the index 'i' from the preview loop
191+ filename_prefix = f"PreviewHistory_Item_{ i :02d} _"
192+
193+ # Get path for temporary preview file
194+ # Note: using dimensions from pil_img directly
195+ full_output_folder , fname , count , subfolder , _ = folder_paths .get_save_image_path (
196+ filename_prefix , self .output_dir , pil_img .width , pil_img .height
197+ )
198+ file = f"{ fname } _{ count :05} _.png" # Temp preview is always png
199+
200+ # Save the image (from history dir or placeholder) to the temporary location for UI preview
201+ pil_img .save (os .path .join (full_output_folder , file ), quality = 95 ) # Good quality for preview
202+
203+ # Append preview info for this image
204+ previews .append ({
205+ "filename" : file ,
206+ "subfolder" : subfolder ,
207+ "type" : self .type
208+ })
209+
210+ except Exception as e :
211+ print (f"[PreviewHistory] Error generating UI preview for image { i } : { e } " )
212+ # Optionally skip this preview or add an error indicator? For now, just skip.
213+
214+
215+ # Return the list of preview data dictionaries
216+ return {"ui" : {"images" : previews }}
217+
218+
219+ NODE_CLASS_MAPPINGS = {
220+ "PreviewHistory" : PreviewHistory
221+ }
222+
223+ NODE_DISPLAY_NAME_MAPPINGS = {
224+ "PreviewHistory" : "Preview History (Raffle)"
225+ }
0 commit comments