text: from __future__ import annotations
  
  import enum
  from functools import partial
  from numbers import Real
  from typing import Any, Callable, Iterable, Optional, Tuple
  
  from dataclassy import dataclass
  from dataclassy.functions import replace
  
  from wordmaze.utils.dataclasses import DataClassSequence, as_dict, as_tuple
  from wordmaze.utils.sequences import MutableSequence
  
  
  @dataclass(iter=True, kwargs=True)
  class Box:
      x1: Real = None
      x2: Real = None
      y1: Real = None
      y2: Real = None
  
      def __post_init__(
          self, height: Optional[Real] = None, width: Optional[Real] = None
      ) -> None:
          if (self.x1, self.x2, width).count(None) != 1:
              raise ValueError(
                  'exactly one of *x1*, *x2*, *width* must be *None* or omitted'
              )
          elif width is not None and width < 0:
              raise ValueError('*width* must be *None* (or omitted) or >=0')
  
          if (self.y1, self.y2, height).count(None) != 1:
              raise ValueError(
                  'exactly one of *y1*, *y2*, *height* must be *None* (or omitted)'
              )
          elif height is not None and height < 0:
              raise ValueError('*height* must be *None* (or omitted) or >=0')
  
          if self.x1 is None:
              self.x1 = self.x2 - width
          elif self.x2 is None:
              self.x2 = self.x1 + width
          elif self.x1 > self.x2:
              self.x2, self.x1 = self.x1, self.x2
  
          if self.y1 is None:
              self.y1 = self.y2 - height
          elif self.y2 is None:
              self.y2 = self.y1 + height
          elif self.y1 > self.y2:
              self.y2, self.y1 = self.y1, self.y2
  
      @property
      def height(self) -> Real:
          return abs(self.y1 - self.y2)
  
      @property
      def width(self) -> Real:
          return abs(self.x1 - self.x2)
  
      @property
      def xmid(self) -> Real:
          return (self.x1 + self.x2) / 2
  
      @property
      def ymid(self) -> Real:
          return (self.y1 + self.y2) / 2
  
  
  class TextBox(Box):
      text: str
      confidence: Real
  
  
  class PageTextBox(TextBox):
      page: int
  
  
  @dataclass(iter=True)
  class Shape:
      height: Real
      width: Real
  
  
  class Origin(enum.Enum):
      TOP_LEFT = enum.auto()
      BOTTOM_LEFT = enum.auto()
  
  
  class Page(DataClassSequence[TextBox]):
      def __init__(
          self,
          shape: Shape,
          entries: Iterable[TextBox] = (),
          origin: Origin = Origin.TOP_LEFT,
      ) -> None:
          super().__init__(entries)
          self.shape: Shape = shape
          self.origin: Origin = origin
  
      def map(
          self,
          *mapper: Callable[[TextBox], TextBox],
          **field_mappers: Callable[[Any], Any],
      ) -> Page:
          return Page(
              shape=self.shape,
              origin=self.origin,
              entries=super().map(*mapper, **field_mappers),
          )
  
      def filter(
          self,
          *pred: Callable[[TextBox], bool],
          **field_preds: Callable[[Any], bool],
      ) -> Page:
          return Page(
              shape=self.shape,
              origin=self.origin,
              entries=super().filter(*pred, **field_preds),
          )
  
      def rebase(self, origin: Origin) -> Page:
          if origin is self.origin:
              return self
          elif (
              origin is Origin.BOTTOM_LEFT and self.origin is Origin.TOP_LEFT
          ) or (origin is Origin.TOP_LEFT and self.origin is Origin.BOTTOM_LEFT):
  
              def rebaser(textbox: TextBox) -> TextBox:
                  return replace(
                      textbox,
                      y1=self.shape.height - textbox.y2,
                      y2=self.shape.height - textbox.y1,
                  )
  
          else:
              raise NotImplementedError(
                  'unsupported rebase operation:'
                  f' from {self.origin} to {origin}.'
              )
  
          rebased = self.map(rebaser)
          rebased.origin = origin
          return rebased
  
  
  class WordMaze(MutableSequence[Page]):
      def __init__(self, pages: Iterable[Page] = ()) -> None:
          super().__init__(pages)
  
      @property
      def shapes(self) -> Tuple[Shape, ...]:
          return tuple(page.shape for page in self)
  
      def textboxes(self) -> Iterable[PageTextBox]:
          return (
              PageTextBox(page=number, **as_dict(textbox))
              for number, page in enumerate(self)
              for textbox in page
          )
  
      def tuples(self) -> Iterable[tuple]:
          return map(partial(as_tuple, flatten=True), self.textboxes())
  
      def dicts(self) -> Iterable[dict]:
          return map(partial(as_dict, flatten=True), self.textboxes())
  
      def map(
          self,
          *mapper: Callable[[TextBox], TextBox],
          **field_mappers: Callable[[Any], Any],
      ) -> WordMaze:
          return WordMaze(page.map(*mapper, **field_mappers) for page in self)
  
      def filter(
          self,
          *pred: Callable[[TextBox], bool],
          **field_preds: Callable[[Any], bool],
      ) -> WordMaze:
          return WordMaze(page.filter(*pred, **field_preds) for page in self)
  