File size: 2,261 Bytes
df84e45
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
"""Contact sheet: every asset of a task in one labelled image.

A vision model on the tools surface cannot open files, so it needs to *see*
which asset is which before it can place `a3` on the canvas. One image with a
labelled tile per asset is far cheaper than one tool call per asset, and the
checkerboard shows which assets are cut-outs and which are opaque photos.
"""

from __future__ import annotations

import io
from pathlib import Path

from PIL import Image, ImageDraw, ImageFont

TILE = 180
LABEL = 26
PAD = 10


def _checker(size: tuple[int, int], cell: int = 10) -> Image.Image:
    img = Image.new("RGB", size, (255, 255, 255))
    draw = ImageDraw.Draw(img)
    for y in range(0, size[1], cell):
        for x in range(0, size[0], cell):
            if (x // cell + y // cell) % 2:
                draw.rectangle([x, y, x + cell - 1, y + cell - 1], fill=(228, 228, 228))
    return img


def contact_sheet(assets: list[dict], paths: dict[str, Path], font_path: Path | None = None,
                  cols: int = 4) -> bytes:
    n = len(assets)
    cols = max(1, min(cols, n))
    rows = (n + cols - 1) // cols
    width = cols * (TILE + PAD) + PAD
    height = rows * (TILE + LABEL + PAD) + PAD
    sheet = Image.new("RGB", (width, max(height, TILE)), (246, 246, 248))
    draw = ImageDraw.Draw(sheet)
    try:
        font = ImageFont.truetype(str(font_path), 16) if font_path else ImageFont.load_default(16)
    except Exception:
        font = ImageFont.load_default()
    for i, asset in enumerate(assets):
        r, c = divmod(i, cols)
        x0 = PAD + c * (TILE + PAD)
        y0 = PAD + r * (TILE + LABEL + PAD)
        tile = _checker((TILE, TILE))
        with Image.open(paths[asset["id"]]) as im:
            im = im.convert("RGBA")
            im.thumbnail((TILE - 8, TILE - 8))
            tile.paste(im, ((TILE - im.width) // 2, (TILE - im.height) // 2), im)
        sheet.paste(tile, (x0, y0))
        draw.rectangle([x0, y0, x0 + TILE - 1, y0 + TILE - 1], outline=(200, 200, 205))
        label = f"{asset['id']}  {asset['kind']}  {asset['aspect']:.2f}:1"
        draw.text((x0 + 2, y0 + TILE + 4), label, fill=(30, 30, 35), font=font)
    buf = io.BytesIO()
    sheet.save(buf, "PNG", optimize=True)
    return buf.getvalue()