160 lines
5.5 KiB
Python
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"}
|
|
]
|