# Signaturassistent
# Version 1.4
# Datei: processor.py

from PIL import Image, ImageFilter, ImageOps

from objects import ConnectedObjectFinder


class SignatureProcessor:
    """
    Neu in 1.4:

    - EXIF-Orientierung wird angewendet. Fotos vom
      Smartphone sind sonst gedreht oder auf dem
      Kopf gespeichert.

    - Das Freistellen (transparent machen) nutzt
      einen adaptiven Schwellwert (Otsu) statt fest
      170. Damit funktionieren auch Fotos, bei denen
      das Papier grau statt weiß erscheint.

    - Findet die Suche mit dem Standard-Schwellwert
      nichts, wird einmal mit adaptivem Schwellwert
      erneut gesucht (Fallback für dunkle Fotos).
    """

    def __init__(self):

        self.finder = ConnectedObjectFinder()

        self.margin = 10

        # Maximale Kantenlänge für die Analyse
        self.analysis_size = 800

    def process(self, filename):

        image = Image.open(filename)

        # Drehung aus den EXIF-Daten anwenden
        # (wichtig bei Smartphone-Fotos)
        image = ImageOps.exif_transpose(image)

        image = image.convert("RGBA")

        # Verkleinerte Kopie für die schnelle Suche
        small, scale = self._downscale(image)

        # Parameter an die Verkleinerung anpassen
        self.finder.min_size = max(
            60,
            int(300 * scale * scale)
        )

        self.finder.merge_distance = max(
            10,
            int(60 * scale)
        )

        self.finder.threshold = 170

        bbox = self.finder.find_signature_bbox(small)

        if bbox is None:

            # Fallback: adaptiver Schwellwert,
            # falls das Bild insgesamt dunkler ist
            adaptive = self._otsu_threshold(small)

            if abs(adaptive - 170) > 10:

                self.finder.threshold = adaptive

                bbox = self.finder.find_signature_bbox(
                    small
                )

        if bbox is None:
            return None

        # Box auf Originalgröße zurückrechnen
        bbox = self._upscale_bbox(bbox, scale, image.size)

        bbox = self._add_margin(bbox, image.size)

        image = image.crop(bbox)

        image = self._make_transparent(image)

        return image

    def _downscale(self, image):

        width, height = image.size

        longest = max(width, height)

        if longest <= self.analysis_size:
            return image, 1.0

        scale = self.analysis_size / longest

        # Erosion: dünne dunkle Striche würden beim
        # Verkleinern sonst über den Schwellwert
        # aufhellen und die Signatur zerfiele in
        # einzelne Fragmente.
        image = image.convert("L").filter(
            ImageFilter.MinFilter(5)
        )

        small = image.resize(
            (
                max(1, int(width * scale)),
                max(1, int(height * scale))
            ),
            Image.BILINEAR
        )

        return small, scale

    def _upscale_bbox(self, bbox, scale, size):

        if scale == 1.0:
            return bbox

        left, top, right, bottom = bbox

        width, height = size

        return (
            max(0, int(left / scale) - 2),
            max(0, int(top / scale) - 2),
            min(width, int(right / scale) + 2),
            min(height, int(bottom / scale) + 2),
        )

    def _add_margin(self, bbox, size):

        left, top, right, bottom = bbox

        width, height = size

        left = max(0, left - self.margin)
        top = max(0, top - self.margin)

        right = min(width, right + self.margin)
        bottom = min(height, bottom + self.margin)

        return (
            left,
            top,
            right,
            bottom
        )

    def _otsu_threshold(self, image):
        """
        Otsu-Verfahren: findet den Schwellwert, der
        das Histogramm optimal in zwei Klassen
        (dunkel / hell) teilt.
        """

        histogram = image.convert("L").histogram()

        total = sum(histogram)

        if total == 0:
            return 170

        sum_all = sum(
            i * histogram[i] for i in range(256)
        )

        sum_dark = 0.0
        count_dark = 0

        best_threshold = 170
        best_variance = 0.0

        for t in range(256):

            count_dark += histogram[t]

            if count_dark == 0:
                continue

            count_bright = total - count_dark

            if count_bright == 0:
                break

            sum_dark += t * histogram[t]

            mean_dark = sum_dark / count_dark
            mean_bright = (
                (sum_all - sum_dark) / count_bright
            )

            variance = (
                count_dark
                * count_bright
                * (mean_dark - mean_bright) ** 2
            )

            if variance > best_variance:
                best_variance = variance
                best_threshold = t

        return best_threshold

    def _make_transparent(self, image):

        gray = image.convert("L")

        # Adaptiver Schwellwert für den Ausschnitt:
        # trennt Tinte und Papier auch dann, wenn
        # das Papier im Foto grau erscheint.
        threshold = self._otsu_threshold(gray)

        src = image.load()
        g = gray.load()

        width, height = image.size

        for y in range(height):

            for x in range(width):

                if g[x, y] < threshold:

                    src[x, y] = (
                        0,
                        0,
                        0,
                        255
                    )

                else:

                    src[x, y] = (
                        255,
                        255,
                        255,
                        0
                    )

        return image

    def save_png(self, image, filename):

        image.save(filename, "PNG")
