MooseCP/tools/list_thumbnails.py
2026-07-21 23:48:58 -07:00

160 lines
5.5 KiB
Python

import os
import base64
import logging
import asyncio
import io
import datetime
import time
from typing import Any, Dict
from PIL import Image, ImageDraw, ImageFont
logger = logging.getLogger("MattCP")
async def handle(args: Dict[str, Any]):
"""
Generates a thumbnail grid of images in a directory.
Returns a combined text list (with global indices) and a visual contact sheet.
"""
dir_path = args.get("path")
page = int(args.get("page", 1))
page_size = int(args.get("page_size", 64))
sort_by = args.get("sort_by", "mtime")
if not dir_path:
return {"text": "Error: Missing path argument"}
if not os.path.isdir(dir_path):
return {"text": f"Error: {dir_path} is not a directory."}
exts = ('.png', '.jpg', '.jpeg', '.webp', '.bmp')
file_info_list = []
# 1. Gather all valid images and their stats
for f in os.listdir(dir_path):
if f.lower().endswith(exts):
full_path = os.path.join(dir_path, f)
try:
stats = os.stat(full_path)
file_info_list.append({
"name": f,
"path": full_path,
"mtime": stats.st_mtime,
"size": stats.st_size
})
except Exception as e:
logger.error(f"Could not stat {f}: {e}")
if not file_info_list:
return {"text": "No supported images found in the directory."}
# 2. Sort the list based on requested criteria
if sort_by == "mtime":
file_info_list.sort(key=lambda x: x["mtime"], reverse=True)
elif sort_by == "size":
file_info_list.sort(key=lambda x: x["size"], reverse=True)
else:
file_info_list.sort(key=lambda x: x["name"].lower())
# 3. Apply Pagination
total_files = len(file_info_list)
start_idx = (page - 1) * page_size
end_idx = start_idx + page_size
paged_files = file_info_list[start_idx:end_idx]
if not paged_files:
return {"text": f"No images found on page {page}."}
# Layout constants
thumb_size = 160
padding = 15
label_height = 35
num_files = len(paged_files)
cols = int(num_files**0.5) if num_files > 0 else 1
if cols == 0: cols = 1
rows = (num_files + cols - 1) // cols
canvas_w = cols * (thumb_size + padding) + padding
canvas_h = rows * (thumb_size + label_height + padding) + padding
# Create dark gray background
canvas = Image.new('RGB', (canvas_w, canvas_h), (30, 30, 30))
draw = ImageDraw.Draw(canvas)
# Attempt to load a bold system font for the indices
font = None
font_paths = [
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf",
"/usr/share/fonts/truetype/freefont/FreeSansBold.ttf",
]
for path in font_paths:
if os.path.exists(path):
try:
font = ImageFont.truetype(path, 20)
break
except Exception:
continue
if font is None:
font = ImageFont.load_default()
file_details = []
# 4. Paste thumbnails and render indices
for i, info in enumerate(paged_files):
filename = info["name"]
full_path = info["path"]
row = i // cols
col = i % cols
x = padding + col * (thumb_size + padding)
y = padding + row * (thumb_size + label_height + padding)
# Continuous indexing across pages
global_index = start_idx + i + 1
try:
with Image.open(full_path) as img:
width, height = img.size
img.thumbnail((thumb_size, thumb_size))
# Center image in its slot
off_x = (thumb_size - img.width) // 2
off_y = (thumb_size - img.height) // 2
canvas.paste(img, (x + off_x, y + off_y))
# Format text metadata
from tools.utils import format_relative_time # Import helper from utils
mod_time_rel = format_relative_time(info["mtime"])
size_kb = info["size"] / 1024
file_details.append(f"{global_index}. {filename} | {width}x{height} | {size_kb:.1f}KB | {mod_time_rel}")
# Draw the index number centered under the image
text = str(global_index)
if font:
bbox = draw.textbbox((0, 0), text, font=font)
text_w = bbox[2] - bbox[0]
else:
text_w = len(text) * 7
text_x = x + (thumb_size - text_w) // 2
draw.text((text_x, y + thumb_size + 2), text, fill=(255, 255, 255), font=font)
except Exception as e:
logger.error(f"Failed to process {filename}: {e}")
draw.text((x, y + thumb_size // 2), "Error", fill=(255, 0, 0), font=font)
file_details.append(f"{global_index}. {filename} | ERROR")
# 5. Encode result to base64 PNG
buf = io.BytesIO()
canvas.save(buf, format='PNG')
img_data = base64.b64encode(buf.getvalue()).decode("utf-8")
total_pages = (total_files + page_size - 1) // page_size
header_text = f"Directory: {dir_path}\nPage {page} of {total_pages} ({total_files} total images). Showing {start_idx+1}-{min(end_idx, total_files)}."
details_text = "\n".join(file_details)
return [
{"type": "text", "text": f"{header_text}\n\nFile List:\n{details_text}"},
{"type": "image", "data": img_data, "mimeType": "image/png"}
]