# Signaturassistent
# Version 1.4
# Datei: objects.py

from collections import deque


class ConnectedObjectFinder:
    """
    Sucht zusammenhängende dunkle Objekte.

    Neu in 1.3:

    - Objekte, die den Bildrand berühren, werden
      ignoriert. Scanner-Schatten und Blattkanten
      liegen immer am Rand, eine Unterschrift nie.

    - Kleinere Objekte in der Nähe des größten
      Objekts (i-Punkte, Umlaute, zweiter Namensteil)
      werden in die Bounding-Box aufgenommen.
    """

    def __init__(
        self,
        threshold=170,
        min_size=300,
        edge_margin=3,
        merge_distance=60
    ):
        self.threshold = threshold
        self.min_size = min_size
        self.edge_margin = edge_margin
        self.merge_distance = merge_distance

    def find_signature_bbox(self, image):
        """
        Liefert die Bounding-Box der Unterschrift
        oder None, wenn keine gefunden wurde.
        """

        components = self._find_components(image)

        if not components:
            return None

        # Größte Komponente = Hauptzug der Unterschrift
        components.sort(key=len, reverse=True)

        largest = components[0]

        if len(largest) < self.min_size:
            return None

        bbox = self._bounding_box(largest)

        # Benachbarte Komponenten einsammeln
        for component in components[1:]:

            other = self._bounding_box(component)

            if self._is_near(bbox, other):
                bbox = self._union(bbox, other)

        return bbox

    def _find_components(self, image):

        gray = image.convert("L")

        width, height = gray.size

        pixels = gray.load()

        visited = [
            [False for _ in range(width)]
            for _ in range(height)
        ]

        components = []

        for y in range(height):

            for x in range(width):

                if visited[y][x]:
                    continue

                visited[y][x] = True

                if pixels[x, y] >= self.threshold:
                    continue

                component, touches_edge = self._floodfill(
                    pixels,
                    visited,
                    width,
                    height,
                    x,
                    y
                )

                # Scanner-Artefakte am Rand verwerfen
                if touches_edge:
                    continue

                components.append(component)

        return components

    def _floodfill(
        self,
        pixels,
        visited,
        width,
        height,
        start_x,
        start_y
    ):

        queue = deque()

        queue.append((start_x, start_y))

        component = []
        touches_edge = False

        m = self.edge_margin

        while queue:

            x, y = queue.popleft()

            component.append((x, y))

            if x < m or y < m \
               or x >= width - m \
               or y >= height - m:
                touches_edge = True

            for dx, dy in (
                (-1, 0),
                (1, 0),
                (0, -1),
                (0, 1),
            ):

                nx = x + dx
                ny = y + dy

                if nx < 0 or ny < 0:
                    continue

                if nx >= width or ny >= height:
                    continue

                if visited[ny][nx]:
                    continue

                visited[ny][nx] = True

                if pixels[nx, ny] >= self.threshold:
                    continue

                queue.append((nx, ny))

        return component, touches_edge

    def _bounding_box(self, component):

        xs = [p[0] for p in component]
        ys = [p[1] for p in component]

        return (
            min(xs),
            min(ys),
            max(xs) + 1,
            max(ys) + 1,
        )

    def _is_near(self, bbox, other):

        d = self.merge_distance

        left, top, right, bottom = bbox

        o_left, o_top, o_right, o_bottom = other

        if o_left > right + d:
            return False

        if o_right < left - d:
            return False

        if o_top > bottom + d:
            return False

        if o_bottom < top - d:
            return False

        return True

    def _union(self, bbox, other):

        return (
            min(bbox[0], other[0]),
            min(bbox[1], other[1]),
            max(bbox[2], other[2]),
            max(bbox[3], other[3]),
        )
