diff --git a/preview_history.py b/preview_history.py index 8141582..c04faf4 100644 --- a/preview_history.py +++ b/preview_history.py @@ -19,7 +19,9 @@ def tensor2pil(image): image = image[0] return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)) -# Placeholder generation function (creates a base image, will be resized later if needed by UI) +# Placeholder generation function (remains unused in the optimized path, keep for robustness?) +# Note: Placeholder generation is removed from the main optimized logic below. +# If an error occurs during copy, the image is simply skipped in the preview. def create_placeholder(size=(128, 128), text="?"): """Creates a simple placeholder PIL image.""" img = Image.new('RGB', size, color = (40, 40, 40)) @@ -27,13 +29,14 @@ def create_placeholder(size=(128, 128), text="?"): font = ImageFont.load_default() # Keep it simple for placeholders try: # Basic centering for default font - tw, th = d.textsize(text, font=font) if hasattr(d, 'textsize') else (10, 10) + tw, th = d.textbbox((0,0), text, font=font)[2:] if hasattr(d, 'textbbox') else (10, 10) # Use textbbox if available d.text(((size[0]-tw)/2, (size[1]-th)/2), text, font=font, fill=(180, 180, 180)) except Exception as e: print(f"[PreviewHistory] Error drawing placeholder text: {e}") d.text((10, 10), text, fill=(180, 180, 180)) # Fallback position return img + class PreviewHistory: # Lock for file system operations in the target directory _dir_lock = threading.Lock() @@ -66,7 +69,6 @@ class PreviewHistory: def execute(self, image, history_size): - # Use the default history folder path directly history_folder = DEFAULT_HISTORY_FOLDER # Ensure target directory exists @@ -76,36 +78,42 @@ class PreviewHistory: print(f"[PreviewHistory] Created history directory: {history_folder}") except OSError as e: print(f"[PreviewHistory] Error creating directory {history_folder}: {e}. Cannot proceed.") - return {"ui": {"images": []}} + # Return empty previews if directory creation fails + return {"ui": {"images": []}} # Return empty list # --- Save New Image (if provided) --- + new_image_saved_path = None if image is not None and image.nelement() > 0: new_pil_image = tensor2pil(image) with PreviewHistory._dir_lock: # Lock specifically for saving try: now = datetime.now() timestamp_final = now.strftime("%d-%m-%Y_%H-%M-%S") + # Include milliseconds for higher uniqueness, preventing rare collisions + timestamp_final += f"_{now.microsecond // 1000:03d}" new_filename = f"history_{timestamp_final}.png" new_path = os.path.join(history_folder, new_filename) - new_pil_image.save(new_path, "PNG", compress_level=1) + new_pil_image.save(new_path, "PNG", compress_level=1) # Faster compression + new_image_saved_path = new_path # Keep track of the path if saved # print(f"[PreviewHistory] Saved new file: {new_path}") # Debug except Exception as e: print(f"[PreviewHistory] Error saving new image to '{history_folder}': {e}") # Continue even if saving fails, try to show existing history - - # --- Cleanup Old Files & Load Images for Preview --- - preview_images_pil = [] - sorted_history_files = [] - with PreviewHistory._dir_lock: # Lock for listing, cleanup, and getting paths + # --- Cleanup Old Files & Get List for Preview --- + sorted_history_files = [] # List to hold full paths of files to preview + with PreviewHistory._dir_lock: # Lock for listing, cleanup try: - # Get all .png files with modification times for cleanup and loading + # Get all .png files with modification times for cleanup all_files = [] for filename in os.listdir(history_folder): if filename.lower().endswith(".png"): full_path = os.path.join(history_folder, filename) try: if os.path.isfile(full_path): + # Use creation time if available and potentially more stable, else modification time + # Note: ctime might be platform-dependent (inode change on Unix, creation on Win) + # Stick to mtime for broader consistency unless ctime is specifically desired mod_time = os.path.getmtime(full_path) all_files.append((mod_time, full_path)) except OSError: @@ -115,89 +123,74 @@ class PreviewHistory: # Sort by modification time, newest first all_files.sort(key=lambda x: x[0], reverse=True) + # Determine which files to keep based on history_size + files_to_keep_info = all_files[:history_size] + files_to_remove_info = all_files[history_size:] + # Cleanup: Remove files exceeding the current history_size - if len(all_files) > history_size: - files_to_remove = all_files[history_size:] # Get the oldest ones - # print(f"[PreviewHistory] Found {len(all_files)} files, keeping {history_size}, removing {len(files_to_remove)}.") # Debug - for mod_time, path_to_remove in files_to_remove: + if files_to_remove_info: + # print(f"[PreviewHistory] Found {len(all_files)} files, keeping {history_size}, removing {len(files_to_remove_info)}.") # Debug + for mod_time, path_to_remove in files_to_remove_info: try: # print(f"[PreviewHistory] Removing old file: {path_to_remove}") # Debug os.remove(path_to_remove) except OSError as e: print(f"[PreviewHistory] Error removing old file {path_to_remove}: {e}") - # Keep only the files that were not removed - files_to_keep = all_files[:history_size] - else: - files_to_keep = all_files # Keep all if within limit - # Get the paths for the files we are keeping for the preview - sorted_history_files = [f[1] for f in files_to_keep] + # Get the final list of full paths for the files we are keeping for the preview + sorted_history_files = [f[1] for f in files_to_keep_info] except Exception as e: - print(f"[PreviewHistory] Error listing/cleaning files in {history_folder} for preview: {e}") + print(f"[PreviewHistory] Error listing/cleaning files in {history_folder}: {e}") # Fall through with empty list if error during file operations - # Determine a consistent size for placeholders if needed - placeholder_size = (128, 128) # Default - if sorted_history_files: # Check if we have any files paths left after potential cleanup - try: - # Try loading the first actual image (newest one) - first_image_path = sorted_history_files[0] - temp_img = Image.open(first_image_path) - placeholder_size = temp_img.size - temp_img.close() - except Exception: - pass # Ignore if it fails, keep default size - - # Load images from the potentially cleaned-up list - for i, fp in enumerate(sorted_history_files): # Iterate through the paths we collected - try: - img = Image.open(fp).convert('RGB') - preview_images_pil.append(img) - except Exception as e: - print(f"[PreviewHistory] Error loading history image '{fp}' for preview: {e}") - # Keep error placeholder for load failures, use index 'i' for label - preview_images_pil.append(create_placeholder(placeholder_size, f"Err {i:02d}")) - - - # --- Generate Preview Data for the UI --- + # --- Generate Preview Data for the UI by Copying Files --- previews = [] preview_server = server.PromptServer.instance # Get server instance - for i, pil_img in enumerate(preview_images_pil): - if pil_img is None: continue # Should not happen with placeholder logic + # Process the sorted list of history files to generate previews + for i, history_file_path in enumerate(sorted_history_files): + if not os.path.exists(history_file_path): + print(f"[PreviewHistory] Warning: File {history_file_path} not found during preview generation (maybe removed?). Skipping.") + continue try: - # Use numpy array for saving temporary preview file - img_array = np.array(pil_img).astype(np.uint8) - # Define a unique prefix for each temp preview file in the batch - # Simplify prefix - just use the index 'i' from the preview loop + # 1. Get image dimensions efficiently + # Use a context manager to ensure the file handle is closed + # PIL often reads headers without loading the full image data + with Image.open(history_file_path) as img: + width, height = img.size + + # 2. Determine temporary path information using dimensions + # Use a distinct prefix based on the original filename hash or index to avoid collisions in temp dir + # Using index 'i' is simple and effective here. filename_prefix = f"PreviewHistory_Item_{i:02d}_" - - # Get path for temporary preview file - # Note: using dimensions from pil_img directly full_output_folder, fname, count, subfolder, _ = folder_paths.get_save_image_path( - filename_prefix, self.output_dir, pil_img.width, pil_img.height + filename_prefix, self.output_dir, width, height ) - file = f"{fname}_{count:05}_.png" # Temp preview is always png + temp_filename = f"{fname}_{count:05}_.png" # Previews are always PNG + temp_file_path = os.path.join(full_output_folder, temp_filename) - # Save the image (from history dir or placeholder) to the temporary location for UI preview - pil_img.save(os.path.join(full_output_folder, file), quality=95) # Good quality for preview + # 3. Copy the file from history folder to temp folder + # shutil.copy2 preserves more metadata (like mtime), copy is slightly faster if metadata isn't needed. + # Using copy should be sufficient here. + shutil.copy(history_file_path, temp_file_path) - # Append preview info for this image + # 4. Append preview info for this image previews.append({ - "filename": file, + "filename": temp_filename, "subfolder": subfolder, "type": self.type }) except Exception as e: - print(f"[PreviewHistory] Error generating UI preview for image {i}: {e}") - # Optionally skip this preview or add an error indicator? For now, just skip. + print(f"[PreviewHistory] Error processing history image '{history_file_path}' for preview: {e}. Skipping this image.") + # Optionally, could create and copy a placeholder image here, but skipping is simpler/faster. # Return the list of preview data dictionaries + # print(f"[PreviewHistory] Generated {len(previews)} previews.") # Debug return {"ui": {"images": previews}}