"""Random selection and layout shared by preview, display and scheduled jobs.""" import random from io import BytesIO from PIL import Image, ImageOps from pigway_frame_flow.errors import ErrorCode as E from pigway_frame_flow.errors import FlowError from pigway_frame_flow.extensions.layout import LayoutInput, LayoutStudio from pigway_frame_flow.models import TemplateContent def prepare(service, content, device_id=None): rng = random.SystemRandom() ids = [] hashes = set() if content.source_id: from pigway_frame_flow.extensions.quality import passes, photo_quality source = service.photo_sources.get(content.source_id) items = service.photo_sources.catalog(content.source_id) candidates = rng.sample(items, min(len(items), content.count * 5)) for item in candidates: try: data, name, _ = service.photo_sources.read( content.source_id, item["id"] ) quality = photo_quality(data) if not passes(quality, source) or quality["sha256"] in hashes: continue if source.avoid_duplicates and quality["fingerprint"] in hashes: continue hashes.update([quality["sha256"], quality["fingerprint"]]) with Image.open(BytesIO(data)) as image: photo = ImageOps.exif_transpose(image).convert("RGB") photo.thumbnail((2400, 2400)) output = BytesIO() photo.save(output, "JPEG", quality=92) asset = service.assets.put(output.getvalue(), name, "cache") if asset["asset_id"] not in ids: ids.append(asset["asset_id"]) except (OSError, ValueError): continue if len(ids) == content.count: break else: rows = service.db.all( "SELECT id AS asset_id FROM assets WHERE kind='images' ORDER BY created_at DESC LIMIT 10000" ) ids = rng.sample( [row["asset_id"] for row in rows], min(content.count, len(rows)) ) if len(ids) != content.count: raise FlowError( E.INVALID_REQUEST, "Not enough distinct photos for requested count", 422 ) values = content.model_dump(exclude={"type", "source_id", "count", "profile"}) if content.orientation == "auto": from pigway_frame_flow.renderers.frame_geometry import frame_canvas display = ( service.registry.get(device_id or service.settings.device_id) .device() .display ) width, height = frame_canvas(display) values["orientation"] = "landscape" if width > height else "portrait" plan = LayoutStudio(service.renderer).plan( LayoutInput( asset_ids=ids, **values, ) ) return TemplateContent( type="template", template="poster" if content.style == "poster" else "collage", data={"layout": plan.model_dump()}, profile=content.profile, )