"""Toglie il fondo bianco alle immagini scatola dei negozi.

I render 3D dei negozi stanno su bianco pieno: su una slide nera farebbero un
quadrato. Si parte dai bordi e si allaga verso l'interno, cosi' i bianchi
*dentro* l'illustrazione (nuvole, denti, cartelli) restano dove sono.

Il riempimento da solo non basta: sul bordo della scatola restano i pixel di
antialiasing, mezzi bianchi, che su fondo nero si vedono come un alone. Per
questo la selezione si stringe di un pixel (`erosione`) prima di salvare:
si perde un pixel di scatola e si guadagna un bordo pulito.
"""
import sys
from collections import deque
from PIL import Image


class NienteFondo(Exception):
    """L'immagine non ha un fondo da togliere: l'illustrazione arriva ai bordi."""


def scontorna(dentro, fuori, soglia=26, erosione=1, ombra=0.0, luce=150):
    """Rende trasparente il fondo bianco attorno alla scatola.

    `ombra` serve alle immagini che sono un **rendering della scatola in
    prospettiva**, come quella italiana di Autobahn: sotto la scatola c'e' una
    sfumatura grigia, e con il solo criterio del bianco resta li' come un alone
    su fondo nero. Segnalato da Paolo il 2026-09-07 guardando post32_4.

    Il valore e' la quota di altezza sotto la quale conta come fondo anche il
    pixel **chiaro e senza colore**, cioe' coi tre canali quasi uguali e
    abbastanza luminoso: `ombra=0.75` vuol dire "solo nell'ultimo quarto".

    **La fascia serve, e non e' prudenza.** Al primo tentativo il criterio del
    grigio era applicato a tutta l'immagine e si e' mangiato il cielo dentro la
    copertina, che e' azzurro pallido e quindi chiaro e quasi senza colore quanto
    l'ombra. L'ombra pero' sta sempre sotto la scatola, il cielo sta sempre in
    alto: limitare la regola alla fascia bassa separa i due casi senza dover
    guardare altro.
    """
    im = Image.open(dentro).convert("RGBA")
    _verifica_fondo(im, soglia)
    w, h = im.size
    px = im.load()
    fuori_mask = bytearray(w * h)          # 1 = fondo, da rendere trasparente
    coda = deque()

    # misurati sull'ombra di Autobahn: 200-254 e canali pari. Su Masterland (24/9) l'ombra del render
    # scende a 137-155, quindi `luce` si abbassa a 120 per quella scatola.
    LUCE, GRIGIO = luce, 14
    da_y = int(h * ombra) if ombra else h + 1

    def bianco(x, y):
        r, g, b, _ = px[x, y]
        if r >= 255 - soglia and g >= 255 - soglia and b >= 255 - soglia:
            return True
        if y >= da_y:
            return max(r, g, b) - min(r, g, b) <= GRIGIO and (r + g + b) / 3 >= LUCE
        return False

    for x in range(w):
        for y in (0, h - 1):
            if bianco(x, y) and not fuori_mask[y * w + x]:
                fuori_mask[y * w + x] = 1; coda.append((x, y))
    for y in range(h):
        for x in (0, w - 1):
            if bianco(x, y) and not fuori_mask[y * w + x]:
                fuori_mask[y * w + x] = 1; coda.append((x, y))

    while coda:
        x, y = coda.popleft()
        for dx, dy in ((1, 0), (-1, 0), (0, 1), (0, -1)):
            nx, ny = x + dx, y + dy
            if 0 <= nx < w and 0 <= ny < h and not fuori_mask[ny * w + nx] and bianco(nx, ny):
                fuori_mask[ny * w + nx] = 1
                coda.append((nx, ny))

    # stringe la selezione: ogni pixel opaco che tocca il fondo diventa fondo
    for _ in range(erosione):
        bordo = [(x, y) for y in range(h) for x in range(w)
                 if not fuori_mask[y * w + x] and any(
                     0 <= x + dx < w and 0 <= y + dy < h and fuori_mask[(y + dy) * w + x + dx]
                     for dx, dy in ((1, 0), (-1, 0), (0, 1), (0, -1)))]
        for x, y in bordo:
            fuori_mask[y * w + x] = 1

    for y in range(h):
        for x in range(w):
            if fuori_mask[y * w + x]:
                px[x, y] = (0, 0, 0, 0)

    bbox = im.getbbox()
    if bbox:
        im = im.crop(bbox)
    im.save(fuori)
    return im.size


def _verifica_fondo(im, soglia, minimo=0.75):
    """Controlla che il bordo sia davvero bianco prima di allagare.

    Su Between Two Cities l'illustrazione arriva fino ai bordi: il riempimento
    e' entrato nel cielo e si e' mangiato le nuvole. Meglio fermarsi e usare il
    jpg cosi' com'e' che consegnare una scatola bucata.
    """
    w, h = im.size
    px = im.load()
    bordo = ([px[x, 0] for x in range(w)] + [px[x, h - 1] for x in range(w)]
             + [px[0, y] for y in range(h)] + [px[w - 1, y] for y in range(h)])
    bianchi = sum(1 for r, g, b, *_ in bordo
                  if r >= 255 - soglia and g >= 255 - soglia and b >= 255 - soglia)
    quota = bianchi / len(bordo)
    if quota < minimo:
        raise NienteFondo(f"solo il {quota:.0%} del bordo e' bianco: l'illustrazione "
                          "arriva ai bordi, usa il jpg senza scontornare")
    return quota


def rifila_bianco(dentro, fuori=None, soglia=20, quota=0.985):
    """Toglie le bande bianche che i negozi aggiungono per fare l'immagine quadrata.

    Non e' lo scontorno: qui non si allaga niente, si tagliano soltanto le righe
    e le colonne ai bordi che sono bianche quasi per intero. Su Terramara la
    scatola e' verticale e il negozio l'ha impaginata su un quadrato, lasciando
    due fasce bianche ai lati che sulla slide nera si vedono benissimo.
    """
    im = Image.open(dentro).convert("RGB")
    w, h = im.size
    px = im.load()
    lim = 255 - soglia

    def bianca_riga(y):
        n = sum(1 for x in range(w) if min(px[x, y]) >= lim)
        return n / w >= quota

    def bianca_col(x):
        n = sum(1 for y in range(h) if min(px[x, y]) >= lim)
        return n / h >= quota

    alto = 0
    while alto < h - 1 and bianca_riga(alto):
        alto += 1
    basso = h - 1
    while basso > alto and bianca_riga(basso):
        basso -= 1
    sx = 0
    while sx < w - 1 and bianca_col(sx):
        sx += 1
    dx = w - 1
    while dx > sx and bianca_col(dx):
        dx -= 1

    tagliata = im.crop((sx, alto, dx + 1, basso + 1))
    if fuori:
        tagliata.save(fuori, quality=95)
    return (w - tagliata.size[0], h - tagliata.size[1]), tagliata.size


def _demo():
    """Un quadrato rosso su bianco: dopo lo scontorno niente deve restare chiaro."""
    from PIL import ImageDraw
    import os, tempfile
    a = os.path.join(tempfile.gettempdir(), "_sc_in.png")
    b = os.path.join(tempfile.gettempdir(), "_sc_out.png")
    im = Image.new("RGB", (60, 60), (255, 255, 255))
    ImageDraw.Draw(im).rectangle([20, 20, 39, 39], fill=(200, 30, 30))
    im.save(a)
    scontorna(a, b)
    out = Image.open(b).convert("RGBA")
    assert out.size == (18, 18), out.size          # 20 meno un pixel per lato
    for p in out.getdata():
        assert p[3] == 0 or p[:3] == (200, 30, 30), p
    print("demo ok:", out.size)


if __name__ == "__main__":
    if len(sys.argv) == 1:
        _demo()
    else:
        print(scontorna(sys.argv[1], sys.argv[2]))
