Exemple #1
0
def main():
    try:
        opts, args = getopt.getopt(sys.argv[1:], "t:o:s:h",
                                   ["text=", "output=", "script=", "help"])
    except getopt.GetoptError as err:
        # print help information and exit:
        print(err)  # will print something like "option -a not recognized"
        usage()
        sys.exit(2)
    output = None
    text = None
    script = None
    for o, a in opts:
        if o in ("-t", "--text"):
            text = a
        elif o in ("-o", "--output"):
            output = a
        elif o in ("-s", "--script"):
            script = a
        elif o in ("-h", "--help"):
            usage()
            sys.exit()
        else:
            assert False, "unhandled option"

    img = scriptList[script](text, method=Image.LANCZOS)

    metadata = PngInfo()
    metadata.add_itxt("Content", text)

    img.save(output, pnginfo=metadata)
Exemple #2
0
    def add_png_metadata(self, path, glyph):
        '''
        given the path to a png file and the glyph it contains,
        write appropriate metadata fields into the file
        '''
        self.metadata['Description'] = self.metadata[
            '_Description_tmpl'].format(glyph=glyph)
        self.metadata['Title'] = self.metadata['_Title_tmpl'].format(
            glyph=glyph,
            border=self.args['bordercolor_name'],
            color=self.args['bgcolor_name'])

        with Image.open(path) as image:
            info = PngInfo()
            for entry in ["Author", "Description", "Title", "Software"]:
                if not entry.startswith('_'):
                    info.add_itxt(entry, self.metadata[entry], "en", entry)
            basename, filename = os.path.split(path)
            newname = os.path.join(basename, 'new-' + filename)
            image.save(newname, pnginfo=info)

            if self.args['verbose']:
                with Image.open(newname) as image:
                    print("new image is", newname)
                    print(image.info)
        os.unlink(path)
        os.rename(newname, path)
Exemple #3
0
class Image:

    def __init__(self, image: ndarray, from_color_space: str = None, to_color_space: str = None):
        raiseif(
            not isinstance(image, ndarray),
            UnearthtimeException(f'Image is type [{type(image)}] and must be type `numpy.ndarray`')
        )

        self.__image, self.__color_space = Image.__resolve_image(image, from_color_space, to_color_space)
        self.__hash = 0
        self.__height, self.__width = image.shape[0], image.shape[1]
        self.__info = PngInfo()

    def __copy__(self):
        return Image(self.__image.copy(), self.__color_space, self.__color_space)

    def __getattr__(self, attr):
        if hasattr(self.__image, attr):
            return getattr(self.__image, attr)

    def __hash__(self):
        if not self.__hash:
            self.__hash = hash((self.__image.data.toreadonly, self.__image.sum(), self.__color_space, self.__width, self.__height))

        return self.__hash

    def __abs__(self):
        return Image(self.__image.__abs__(), self.__color_space)

    def __add__(self, value, /):
        return Image(self.__image.__add__(value), self.__color_space)

    def __and__(self, value, /):
        return Image(self.__image.__and__(value), self.__color_space)

    def __bool__(self):
        return self.__image.__bool__()

    def __complex__(self):
        return self.__image.__complex__()

    def __contains__(self, value, /):
        return self.__image.__contains__(value)

    def __delitem__(self, value, /):
        self.__image.__delitem__(value)

    def __divmod__(self, value, /):
        d, m = self.__image.__divmod__(value)
        return Image(d, self.__color_space), Image(m, self.__color_space)

    def __eq__(self, value, /):
        return self.__image.__eq__(value)

    def __float__(self):
        return self.__image.__float__()

    def __floordiv__(self, value, /):
        return Image(self.__image.__floordiv__(value), self.__color_space)

    def __getitem__(self, value, /):
        return self.__image.__getitem__(value)

    def __ge__(self, value, /):
        return self.__image.__ge__(value)

    def __gt__(self, value, /):
        return self.__image.__gt__(value)

    def __iadd__(self, value, /):
        self.__image.__iadd__(value)
        return self

    def __iand__(self, value, /):
        self.__image.__iand__(value)
        return self

    def __ifloordiv__(self, value, /):
        self.__image.__ifloordiv__(value)
        return self

    def __ilshift__(self, value, /):
        self.__image.__ilshift__(value)
        return self

    def __imatmul__(self, value, /):
        self.__image.__imatmul__(value)
        return self

    def __imod__(self, value, /):
        self.__image.__imod__(value)
        return self

    def __imul__(self, value, /):
        self.__image.__imul__(value)
        return self

    def __index__(self):
        return self.__image.__index__()

    def __int__(self):
        return self.__image.__int__()

    def __invert__(self):
        return Image(self.__image.__invert__(), self.__color_space)

    def __ior__(self, value, /):
        self.__image.__ior__(value)
        return self

    def __ipow__(self, value, /):
        self.__image.__ipow__(value)
        return self

    def __irshift__(self, value, /):
        self.__image.__irshift__(value)
        return self

    def __isub__(self, value, /):
        self.__image.__isub__(value)
        return self

    def __iter__(self):
        return self.__image.__iter__()

    def __itruediv__(self, value, /):
        self.__image.__itruediv__(value)
        return self

    def __ixor__(self, value, /):
        self.__image.__ixor__(value)
        return self

    def __len__(self):
        return self.__image.__len__()

    def __le__(self, value, /):
        return self.__image.__le__(value)

    def __lshift__(self, value, /):
        return Image(self.__image.__lshift__(value), self.__color_space)

    def __lt__(self, value, /):
        return self.__image.__lt__(value)

    def __matmul__(self, value, /):
        return Image(self.__image.__matmul__(value), self.__color_space)

    def __mod__(self, value, /):
        return Image(self.__image.__mod__(value), self.__color_space)

    def __mul__(self, value, /):
        return Image(self.__image.__mul__(value), self.__color_space)

    def __neg__(self):
        return Image(self.__image.__neg__(), self.__color_space)

    def __ne__(self, value, /):
        return self.__image.__ne__(value)

    def __or__(self, value, /):
        return Image(self.__image.__or__(value), self.__color_space)

    def __pos__(self):
        return Image(self.__image.__pos__(), self.__color_space)

    def __pow__(self, value, /):
        return Image(self.__image.__pow__(value), self.__color_space)

    def __radd__(self, value, /):
        return Image(self.__image.__radd__(value), self.__color_space)

    def __rand__(self, value, /):
        return Image(self.__image.__rand__(value), self.__color_space)

    def __rdivmod__(self, value, /):
        d, m = self.__image.__rdivmod__(self, value)
        return Image(d, self.__color_space), Image(m, self.__color_space)

    def __rfloordiv__(self, value, /):
        return Image(self.__image.__rfloordiv__(value), self.__color_space)

    def __rlshift__(self, value, /):
        return Image(self.__image.__rlshift__(value), self.__color_space)

    def __rmatmul__(self, value, /):
        return Image(self.__image.__rmatmul__(value), self.__color_space)

    def __rmod__(self, value, /):
        return Image(self.__image.__rmod__(value), self.__color_space)

    def __rmul__(self, value, /):
        return Image(self.__image.__rmul__(value), self.__color_space)

    def __ror__(self, value, /):
        return Image(self.__image.__ror__(value), self.__color_space)

    def __rpow__(self, value, /):
        return Image(self.__image.__rpow__(value), self.__color_space)

    def __rrshift__(self, value, /):
        return Image(self.__image.__rrshift__(value), self.__color_space)

    def __rshift__(self, value, /):
        return Image(self.__image.__rshift__(value), self.__color_space)

    def __rsub__(self, value, /):
        return Image(self.__image.__rsub__(value), self.__color_space)

    def __rtruediv__(self, value, /):
        return Image(self.__image.__rtruediv__(value), self.__color_space)

    def __rxor__(self, value, /):
        return Image(self.__image.__rxor__(value), self.__color_space)

    def __setitem__(self, value, /):
        self.__image.__setitem__(value)
        self.__hash = 0

    def __sizeof__(self):
        return self.__image.__sizeof__()

    def __sub__(self, value, /):
        return Image(self.__image.__sub__(value), self.__color_space)

    def __truediv__(self, value, /):
        return Image(self.__image.__truediv__(value), self.__color_space)

    def __xor__(self, value, /):
        return Image(self.__image.__xor__(value), self.__color_space)

    @classmethod
    def from_base64(cls, base64: str, to_color_space: str = 'RGBA'):
        return cls(array(PILImage.open(BytesIO(a2b_base64(base64)))), 'RGBA', to_color_space)

    @classmethod
    def from_bytes(cls, bytes_: bytes, to_color_space: str = 'RGBA'):
        return cls(array(PILImage.open(BytesIO(bytes_))), 'RGBA', to_color_space)

    @classmethod
    def from_image(cls, img: PILImage, to_color_space: str = None):
        im = cls(array(img), img.mode, to_color_space)

        if isinstance(img, PngImageFile):
            if img.text:
                for key, value in img.text.items():
                    im.add_text(key, value)

        return im

    @classmethod
    def read_file(cls, fp: str, flags=None, to_color_space: str = 'BGR'):
        return cls(cv.imread(fp, flags), 'BGR', to_color_space=to_color_space)

    @classmethod
    def read_url(cls, url: str, from_color_space: str = 'RGB', to_color_space: str = 'RGB', plugin=None, **plugin_args):
        tcs = to_color_space.upper()

        img = skio.imread(url, as_gray=tcs in ('GRAY', 'GREY'), plugin=plugin, **plugin_args)

        if tcs in ('GRAY', 'GREY'):
            return cls(img, 'GRAY', 'GRAY')
        else:
            return cls(img, from_color_space, to_color_space)

    @property
    def array(self):
        return self.__image

    @property
    def color_space(self):
        return self.__color_space

    @property
    def height(self):
        return self.__height

    @property
    def png_info(self): return self.__info

    @property
    def width(self):
        return self.__width

    def add_text(self, key, value, zip_: bool = False):
        self.__info.add_text(key, value, zip_)

    def add_itxt(self, key, value, lang: str = "", tkey: str = "", zip_: bool = False):
        self.__info.add_itxt(key, value, lang, tkey, zip_)

    def as_image(self):
        return PILImage.fromarray(self.__image)

    def change_color_space(self, color_space: str):
        self.__image, self.__color_space = Image.__resolve_image(self.__image, self.__color_space, color_space)
        self.__hash = 0

    def compare_full(self, img: Image, rect_color: RGBColor = (0, 0, 255), line_thickness=1, line_type=cv.LINE_8):
        cim1, cim2 = self.with_color_space('BGR'), img.with_color_space('BGR')
        gim1, gim2 = cim1.with_color_space('GRAY'), cim2.with_color_space('GRAY')
        score, diff = gim1.compare_ssim(gim2, full=True)
        diff = (diff * 255).astype("uint8")
        threshold = cv.threshold(diff, 0, 255, cv.THRESH_BINARY_INV | cv.THRESH_OTSU)[1]
        contours = im.grab_contours(cv.findContours(threshold.copy(), cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE))

        for contour in contours:
            x, y, w, h = cv.boundingRect(contour)

            cv.rectangle(cim1.array, (x, y), (x + w, y + h), rect_color, line_thickness, line_type)
            cv.rectangle(cim2.array, (x, y), (x + w, y + h), rect_color, line_thickness, line_type)

        mse_ = mse(gim1.array, gim2.array)

        return {
            "mse": mse_,
            "ssim": score,
            "image1": cim1,
            "image2": cim2,
            "diff": diff,
            "threshold": threshold
        }

    def compare_mse(self, img: Image):
        return mse(self.with_color_space('GRAY').array, img.with_color_space('GRAY').array)

    def compare_ssim(self, img: Image, **kwargs):
        return ssim(self.with_color_space('GRAY').array, img.with_color_space('GRAY').array, **kwargs)

    def copy(self):
        return self.__copy__()

    def draw_rectangle(self, pt1, pt2, color: RGBColor = (0, 0, 255), line_thickness: int = 1, line_type: int = cv.LINE_8):
        cv.rectangle(self.__image, pt1, pt2, color, line_thickness, line_type)

    def save(self, fp: str, format_=None, pnginfo=None, **params):
        if (format_ and format_.lower() == 'png') or (fp and fp.endswith('.png')):
            info = self.__info
            
            if pnginfo:
                if isinstance(pnginfo, PngInfo):
                    overriding('stored png info.')
                    info = pnginfo
                else:
                    if not isinstance(pnginfo, dict):
                        pnginfo = dict(pnginfo)
                
                    for name, value in pnginfo.items():
                        info.add_text(name, str(value))
                    
            self.as_image().save(fp, format_, pnginfo=info, **params)
        else:
            self.as_image().save(fp, format_, **params)

    def show(self, name: str):
        cv.imshow(name, self.__image)

    def with_color_space(self, color_space: str):
        return Image(self.__image.copy(), self.__color_space, color_space)

    @staticmethod
    def __resolve_image(image: ndarray, from_color_space: str, to_color_space: str):
        if from_color_space is not None:
            fcs = from_color_space.upper()
        else:
            fcs = 'GRAY' if image.ndim == 2 else 'RGB' if image.ndim == 3 else 'RGBA'

        if to_color_space is not None:
            tcs = to_color_space.upper()
        else:
            tcs = fcs

        raiseif(
            len(set([cs for cs in dir(cv) if f'COLOR_{fcs}' in cs])) == 0,
            UnearthtimeException(f':[{fcs}2{tcs}]: Invalid color space conversion.')
        )

        if fcs == 'GREY':
            fcs = 'GRAY'

        if tcs == 'GREY':
            tcs = 'GRAY'
            
        if fcs != tcs:
            cvtcs = f'COLOR_{fcs}2{tcs}'

            try:
                code = getattr(cv, cvtcs)
            except AttributeError:
                raise UnearthtimeException(f':[{fcs}2{tcs}]: Invalid color space conversion.')

            image = cv.cvtColor(image, code)

        return image, tcs
Exemple #4
0
class MainWindow(QMainWindow):
    """Main application window"""
    def __init__(self) -> None:
        QMainWindow.__init__(self)
        self.setSizePolicy(
            QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum))
        self.setMaximumSize(QSize(1920, 1080))
        self.setStyleSheet("padding: 0px; margin: 0px;")
        self.setIconSize(QSize(32, 32))
        self.setWindowTitle("BossyBot 2000 - Image Tagger")
        self.setWindowIcon(self.load_icon(icon))

        self.menubar = QMenuBar(self)
        self.menubar.setSizePolicy(EXP_MAX)
        self.menubar.setMaximumSize(QSize(INFINITE, 30))
        self.menu_file = QMenu('File', self.menubar)
        self.menu_options = QMenu('Options', self.menubar)
        self.menu_help = QMenu('Help', self.menubar)
        self.menubar.addAction(self.menu_file.menuAction())
        self.menubar.addAction(self.menu_options.menuAction())
        self.menubar.addAction(self.menu_help.menuAction())
        self.open = QAction('Open', self)
        self.menu_file.addAction(self.open)
        self.open.triggered.connect(self.open_file)
        self.exit_button = QAction('Exit', self)
        self.exit_button.triggered.connect(lambda: sys.exit(0),
                                           Qt.QueuedConnection)
        self.menu_file.addAction(self.exit_button)
        self.setMenuBar(self.menubar)

        self.previous_button = QAction(self.load_icon(previous), '<<', self)
        self.next_button = QAction(self.load_icon(next_icon), '>>', self)
        self.rotate_left_button = QAction(self.load_icon(left), '', self)
        self.rotate_right_button = QAction(self.load_icon(right), '', self)
        self.play_button = QAction(self.load_icon(play), '', self)
        self.play_button.setCheckable(True)
        self.delete_button = QAction(self.load_icon(delete), '', self)
        self.reload_button = QAction(self.load_icon(reload), '', self)
        self.mirror_button = QAction('Mirror', self)
        self.actual_size_button = QAction('Actual Size', self)
        self.browser_button = QAction('Browser', self)
        self.browser_button.setCheckable(True)
        self.browser_button.setChecked(True)
        self.crop_button = QAction('Crop', self)
        self.crop_button.setCheckable(True)

        self.toolbuttons = {
            self.rotate_left_button: {
                'shortcut':
                ',',
                'connect':
                lambda: self.pixmap.setRotation(self.pixmap.rotation() - 90)
            },
            self.rotate_right_button: {
                'shortcut':
                '.',
                'connect':
                lambda: self.pixmap.setRotation(self.pixmap.rotation() + 90)
            },
            self.delete_button: {
                'shortcut': 'Del',
                'connect': self.delete
            },
            self.previous_button: {
                'shortcut': 'Left',
                'connect': self.previous
            },
            self.play_button: {
                'shortcut': 'Space',
                'connect': self.play
            },
            self.next_button: {
                'shortcut': 'Right',
                'connect': self.next
            },
            self.reload_button: {
                'shortcut': 'F5',
                'connect': self.reload
            }
        }

        self.toolbar = QToolBar(self)
        self.toolbar.setSizePolicy(EXP_MAX)
        self.toolbar.setMaximumSize(QSize(INFINITE, 27))
        for _ in (self.browser_button, self.crop_button, self.mirror_button,
                  self.actual_size_button):
            self.toolbar.addAction(_)
        self.addToolBar(Qt.TopToolBarArea, self.toolbar)

        for button in self.toolbuttons:
            button.setShortcut(self.toolbuttons[button]['shortcut'])
            button.triggered.connect(self.toolbuttons[button]['connect'])
            self.toolbar.addAction(button)

        self.centralwidget = QWidget(self)
        self.centralwidget.setSizePolicy(EXP_EXP)
        self.setCentralWidget(self.centralwidget)
        self.grid = QGridLayout(self.centralwidget)

        self.media = QGraphicsScene(self)
        self.media.setItemIndexMethod(QGraphicsScene.NoIndex)
        self.media.setBackgroundBrush(QBrush(Qt.black))
        self.view = MyView(self.media, self)
        self.view.setSizePolicy(EXP_EXP)
        self.media.setSceneRect(0, 0, self.view.width(), self.view.height())
        self.grid.addWidget(self.view, 0, 0, 1, 1)

        self.frame = QFrame(self.centralwidget)
        self.frame.setSizePolicy(
            QSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding))
        self.frame.setMinimumSize(QSize(325, 500))
        self.frame.setStyleSheet(
            "QFrame { border: 4px inset #222; border-radius: 10; }")

        self.layout_widget = QWidget(self.frame)
        self.layout_widget.setGeometry(QRect(0, 400, 321, 91))
        self.layout_widget.setContentsMargins(15, 15, 15, 15)

        self.grid2 = QGridLayout(self.layout_widget)
        self.grid2.setContentsMargins(0, 0, 0, 0)

        self.save_button = QPushButton('Yes (Save)', self.layout_widget)
        self.save_button.setSizePolicy(FIX_FIX)
        self.save_button.setMaximumSize(QSize(120, 26))
        self.save_button.setVisible(False)
        self.grid2.addWidget(self.save_button, 1, 0, 1, 1)

        self.no_save_button = QPushButton('No (Reload)', self.layout_widget)
        self.no_save_button.setSizePolicy(FIX_FIX)
        self.no_save_button.setMaximumSize(QSize(120, 26))
        self.no_save_button.setVisible(False)
        self.grid2.addWidget(self.no_save_button, 1, 1, 1, 1)

        self.label = QLabel("Current image modified, save it?",
                            self.layout_widget)
        self.label.setSizePolicy(FIX_FIX)
        self.label.setMaximumSize(QSize(325, 60))
        self.label.setVisible(False)
        self.label.setAlignment(Qt.AlignCenter)
        self.grid2.addWidget(self.label, 0, 0, 1, 2)

        self.layout_widget = QWidget(self.frame)
        self.layout_widget.setGeometry(QRect(0, 0, 321, 213))

        self.ass = QRadioButton('Ass', self.layout_widget)
        self.ass_exposed = QRadioButton('Ass (exposed)', self.layout_widget)
        self.ass_reset = QRadioButton(self.frame)
        self.ass_group = QButtonGroup(self)

        self.breasts = QRadioButton('Breasts', self.layout_widget)
        self.breasts_exposed = QRadioButton('Breasts (exposed)',
                                            self.layout_widget)
        self.breasts_reset = QRadioButton(self.frame)
        self.breasts_group = QButtonGroup(self)

        self.pussy = QRadioButton('Pussy', self.layout_widget)
        self.pussy_exposed = QRadioButton('Pussy (exposed)',
                                          self.layout_widget)
        self.pussy_reset = QRadioButton(self.frame)
        self.pussy_group = QButtonGroup(self)

        self.fully_clothed = QRadioButton('Fully Clothed', self.layout_widget)
        self.fully_nude = QRadioButton('Fully Nude', self.layout_widget)
        self.nudity_reset = QRadioButton(self.frame)
        self.nudity = QButtonGroup(self)

        self.smiling = QRadioButton('Smiling', self.layout_widget)
        self.glaring = QRadioButton('Glaring', self.layout_widget)
        self.expression_reset = QRadioButton(self.frame)
        self.expression = QButtonGroup(self)

        self.grid3 = QGridLayout(self.layout_widget)
        self.grid3.setVerticalSpacing(15)
        self.grid3.setContentsMargins(0, 15, 0, 0)

        self.radios = {
            self.ass: {
                'this': 'ass',
                'that': 'ass_exposed',
                'group': self.ass_group,
                'reset': self.ass_reset,
                'grid': (0, 0, 1, 1)
            },
            self.ass_exposed: {
                'this': 'ass_exposed',
                'that': 'ass',
                'group': self.ass_group,
                'reset': self.ass_reset,
                'grid': (0, 1, 1, 1)
            },
            self.breasts: {
                'this': 'breasts',
                'that': 'breasts_exposed',
                'group': self.breasts_group,
                'reset': self.breasts_reset,
                'grid': (1, 0, 1, 1)
            },
            self.breasts_exposed: {
                'this': 'breasts_exposed',
                'that': 'breasts',
                'group': self.breasts_group,
                'reset': self.breasts_reset,
                'grid': (1, 1, 1, 1)
            },
            self.pussy: {
                'this': 'pussy',
                'that': 'pussy_exposed',
                'group': self.pussy_group,
                'reset': self.pussy_reset,
                'grid': (2, 0, 1, 1)
            },
            self.pussy_exposed: {
                'this': 'pussy_exposed',
                'that': 'pussy',
                'group': self.pussy_group,
                'reset': self.pussy_reset,
                'grid': (2, 1, 1, 1)
            },
            self.fully_clothed: {
                'this': 'fully_clothed',
                'that': 'fully_nude',
                'group': self.nudity,
                'reset': self.nudity_reset,
                'grid': (3, 0, 1, 1)
            },
            self.fully_nude: {
                'this': 'fully_nude',
                'that': 'fully_clothed',
                'group': self.nudity,
                'reset': self.nudity_reset,
                'grid': (3, 1, 1, 1)
            },
            self.smiling: {
                'this': 'smiling',
                'that': 'glaring',
                'group': self.expression,
                'reset': self.expression_reset,
                'grid': (4, 0, 1, 1)
            },
            self.glaring: {
                'this': 'glaring',
                'that': 'smiling',
                'group': self.expression,
                'reset': self.expression_reset,
                'grid': (4, 1, 1, 1)
            },
        }

        for radio in self.radios:
            radio.setSizePolicy(FIX_FIX)
            radio.setMaximumSize(QSize(150, 22))
            self.radios[radio]['reset'].setGeometry(QRect(0, 0, 0, 0))
            self.grid3.addWidget(radio, *self.radios[radio]['grid'])
            if self.radios[radio]['group'] != self.nudity:
                radio.toggled.connect(
                    lambda x=_, y=radio: self.annotate(self.radios[y]['this']))
            self.radios[radio]['group'].addButton(radio)
            self.radios[radio]['group'].addButton(self.radios[radio]['reset'])

        self.save_tags_button = QPushButton('Save Tags', self.layout_widget)
        self.save_tags_button.setSizePolicy(FIX_FIX)
        self.save_tags_button.setMaximumSize(QSize(120, 26))
        self.grid3.addWidget(self.save_tags_button, 5, 1, 1, 1)

        self.grid.addWidget(self.frame, 0, 1, 1, 1)

        self.browse_bar = QLabel(self.centralwidget)
        self.browse_bar.setSizePolicy(EXP_FIX)
        self.browse_bar.setMinimumSize(QSize(0, 100))
        self.browse_bar.setMaximumSize(QSize(INFINITE, 100))
        self.browse_bar.setStyleSheet("background: #000;")
        self.browse_bar.setAlignment(Qt.AlignCenter)
        self.h_box2 = QHBoxLayout(self.browse_bar)
        self.h_box2.setContentsMargins(4, 0, 0, 0)

        self.grid.addWidget(self.browse_bar, 1, 0, 1, 2)

        hiders = [
            self.no_save_button.clicked, self.save_button.clicked,
            self.reload_button.triggered
        ]
        for hider in hiders:
            hider.connect(self.save_button.hide)
            hider.connect(self.no_save_button.hide)
            hider.connect(self.label.hide)
        showers = [
            self.mirror_button.triggered, self.rotate_right_button.triggered,
            self.rotate_left_button.triggered
        ]
        for shower in showers:
            shower.connect(self.save_button.show)
            shower.connect(self.no_save_button.show)
            shower.connect(self.label.show)

        self.no_save_button.clicked.connect(self.reload)
        self.browser_button.toggled.connect(self.browse_bar.setVisible)

        self.play_button.toggled.connect(lambda: self.frame.setVisible(
            (True, False)[self.frame.isVisible()]))
        self.reload_button.triggered.connect(self.reload)
        self.mirror_button.triggered.connect(lambda: self.pixmap.setScale(-1))
        self.save_button.clicked.connect(self.save_image)
        self.play_button.toggled.connect(
            lambda: self.browser_button.setChecked(
                (True, False)[self.browse_bar.isVisible()]))
        self.crop_button.toggled.connect(self.view.reset)
        self.actual_size_button.triggered.connect(self.actual_size)
        self.browser_button.triggered.connect(self.browser)
        self.save_tags_button.clicked.connect(self.save_tags)
        self.view.got_rect.connect(self.set_rect)

        self.crop_rect = QRect(QPoint(0, 0), QSize(0, 0))
        self.dir_now = os.getcwd()
        self.files = []
        self.index = 0
        self.refresh_files()
        self.pixmap_is_scaled = False
        self.pixmap = QGraphicsPixmapItem()
        self.active_tag = ''
        self.reset_browser = False
        self.txt = PngInfo()

    def set_rect(self, rect: tuple[QPointF, QPointF]):
        """Converts the crop rectangle to a QRect after a crop action"""
        self.crop_rect = QRect(rect[0].toPoint(), rect[1].toPoint())

    def keyPressEvent(self, event: QKeyEvent):  # pylint: disable=invalid-name;
        """Keyboard event handler."""
        if event.key() == Qt.Key_Escape and self.play_button.isChecked():
            self.play_button.toggle()
            self.browser_button.setChecked((True, False)[self.reset_browser])
        elif (event.key() in [16777220, 16777221]
              and self.view.g_rect.rect().width() > 0):
            self.view.got_rect.emit((self.view.g_rect.rect().topLeft(),
                                     self.view.g_rect.rect().bottomRight()))
            if self.view.g_rect.pen().color() == Qt.red:
                new_pix = self.pixmap.pixmap().copy(self.crop_rect)
                if self.pixmap_is_scaled:
                    new_pix = new_pix.transformed(
                        self.view.transform().inverted()[0],
                        Qt.SmoothTransformation)
                self.update_pixmap(new_pix)
            elif self.view.g_rect.pen().color() == Qt.magenta:
                self.annotate_rect()
                self.view.annotation = False
            for _ in (self.label, self.save_button, self.no_save_button):
                _.show()
            self.view.reset()

    def play(self):
        """Starts a slideshow."""
        if self.play_button.isChecked():
            if self.browser_button.isChecked():
                self.reset_browser = True
            else:
                self.reset_browser = False
            QTimer.singleShot(3000, self.play)
            self.next()

    def _yield_radio(self):
        """Saves code connecting signals from all the radio buttons."""
        yield from self.radios.keys().__str__()

    def load_icon(self, icon_file):
        """Loads an icon from Base64 encoded strings in icons.py."""
        pix = QPixmap()
        pix.loadFromData(icon_file)
        return QIcon(pix)

    def open_file(self, file: str) -> None:
        """
        Open an image file and display it.

        :param file: The filename of the image to open
        """
        if not os.path.isfile(file):
            file = QFileDialog(self, self.dir_now,
                               self.dir_now).getOpenFileName()[0]
            self.dir_now = os.path.dirname(file)
            self.refresh_files()
        for i, index_file in enumerate(self.files):
            if file.split('/')[-1] == index_file:
                self.index = i
        self.view.setTransform(QTransform())
        self.update_pixmap(QPixmap(file))
        self.browser()
        self.load_tags()

    def refresh_files(self) -> list[str]:
        """Updates the file list when the directory is changed.
        Returns a list of image files available in the current directory."""
        files = os.listdir(self.dir_now)
        self.files = [
            file for file in sorted(files, key=lambda x: x.lower())
            if file.endswith((".png", ".jpg", ".gif", ".bmp", ".jpeg"))
        ]

    def next(self) -> None:
        """Opens the next image in the file list."""
        self.index = (self.index + 1) % len(self.files)
        self.reload()

    def previous(self) -> None:
        """Opens the previous image in the file list."""
        self.index = (self.index + (len(self.files) - 1)) % len(self.files)
        self.reload()

    def save_image(self) -> None:
        """
        Save the modified image file.  If the current pixmap has been
        scaled, we need to load a non-scaled pixmap from the original file and
        re-apply the transformations that have been performed to prevent it
        from being saved as the scaled-down image.
        """
        if self.pixmap_is_scaled:
            rotation = self.pixmap.rotation()
            mirror = self.pixmap.scale() < 0
            pix = QPixmap(self.files[self.index])
            pix = pix.transformed(QTransform().rotate(rotation))
            if mirror:
                pix = pix.transformed(QTransform().scale(-1, 1))
            pix.save(self.files[self.index], quality=-1)
        else:
            self.pixmap.pixmap().save(self.files[self.index], quality=-1)
        self.save_tags()

    def delete(self) -> None:
        """Deletes the current image from the file system."""
        with suppress(OSError):
            os.remove(f"{self.dir_now}/{self.files.pop(self.index)}")
        self.refresh_files()

    def reload(self) -> None:
        """Reloads the current pixmap; used to update the screen when the
        current file is changed."""
        self.open_file(f"{self.dir_now}/{self.files[self.index]}")

    def annotate(self, tag):
        """Starts an annotate action"""
        self.txt = PngInfo()
        self.view.annotation = True
        self.active_tag = tag
        self.view.reset()

    def wheelEvent(self, event: QWheelEvent) -> None:  # pylint: disable=invalid-name
        """With Ctrl depressed, zoom the current image, otherwise fire the
        next/previous functions."""
        modifiers = QApplication.keyboardModifiers()
        if event.angleDelta().y() == 120 and modifiers == Qt.ControlModifier:
            self.view.scale(0.75, 0.75)
        elif event.angleDelta().y() == 120:
            self.previous()
        elif event.angleDelta().y(
        ) == -120 and modifiers == Qt.ControlModifier:
            self.view.scale(1.25, 1.25)
        elif event.angleDelta().y() == -120:
            self.next()

    def actual_size(self) -> None:
        """Display the current image at its actual size, rather than scaled to
        fit the viewport."""
        self.update_pixmap(QPixmap(self.files[self.index]), False)
        self.view.setDragMode(QGraphicsView.ScrollHandDrag)

    def mousePressEvent(self, event: QMouseEvent) -> None:  # pylint: disable=invalid-name
        """Event handler for mouse button presses."""
        if event.button() == Qt.MouseButton.ForwardButton:
            self.next()
        elif event.button() == Qt.MouseButton.BackButton:
            self.previous()

    def update_pixmap(self, new: QPixmap, scaled: bool = True) -> None:
        """
        Updates the currently displayed image.

        :param new: The new `QPixmap` to be displayed.
        :param scaled: If False, don't scale the image to fit the viewport.
        """
        self.pixmap_is_scaled = scaled
        self.media.clear()
        self.pixmap = self.media.addPixmap(new)
        self.pixmap.setTransformOriginPoint(
            self.pixmap.boundingRect().width() / 2,
            self.pixmap.boundingRect().height() / 2)
        if scaled and (new.size().width() > self.view.width()
                       or new.size().height() > self.view.height()):
            self.view.fitInView(self.pixmap, Qt.KeepAspectRatio)
        self.media.setSceneRect(self.pixmap.boundingRect())

    def annotate_rect(self):
        """Creates image coordinate annotation data."""
        self.txt.add_itxt(
            f'{str(self.active_tag)}-rect',
            f'{str(self.crop_rect.x())}, {str(self.crop_rect.y())}, {str(self.crop_rect.width())}, {str(self.crop_rect.height())}'
        )

    def browser(self):
        """Slot function to initialize image thumbnails for the
        'browse mode.'"""
        while self.h_box2.itemAt(0):
            self.h_box2.takeAt(0).widget().deleteLater()
        index = (self.index + (len(self.files) - 2)) % len(self.files)
        for i, file in enumerate(self.files):
            file = self.dir_now + '/' + self.files[index]
            label = ClickableLabel(self, file)
            self.h_box2.addWidget(label)
            pix = QPixmap(file)
            if (pix.size().width() > self.browse_bar.width() / 5
                    or pix.size().height() > 100):
                pix = pix.scaled(self.browse_bar.width() / 5, 100,
                                 Qt.KeepAspectRatio)
            label.setPixmap(pix)
            index = (index + 1) % len(self.files)
            if i == 4:
                break

    def save_tags(self):
        """Save tags for currently loaded image into its iTxt data."""
        file = self.files[self.index]
        img = Image.open(file)
        img.load()
        for key, value, in img.text.items():
            self.txt.add_itxt(key, value)
        for key in self.radios:
            if key.isChecked():
                self.txt.add_itxt(self.radios[key]['this'], 'True')
                self.txt.add_itxt(self.radios[key]['that'], 'False')
        img.save(file, pnginfo=self.txt)

    def load_tags(self):
        """Load tags from iTxt data."""
        for radio in self.radios:
            if radio.isChecked():
                self.radios[radio]['reset'].setChecked(True)
        filename = self.files[self.index]
        fqp = filename
        img = Image.open(fqp)
        img.load()
        with suppress(AttributeError):
            for key, value in img.text.items():
                if value == 'True':
                    for radio in self.radios:
                        if key == self.radios[radio]['this']:
                            radio.setChecked(True)
                            self.view.annotation = False
                            self.active_tag = ''
                            self.view.reset()
            for key, value in img.text.items():
                if key.endswith('-rect'):
                    btn = [
                        radio for radio in self.radios
                        if self.radios[radio]['this'] == key.split('-')[0]
                    ]
                    print(key, value)
                    if btn[0].isChecked():
                        coords = [int(coord) for coord in value.split(', ')]
                        rect = QGraphicsRectItem(*coords)
                        rect.setPen(QPen(Qt.magenta, 1, Qt.SolidLine))
                        rect.setBrush(QBrush(Qt.magenta, Qt.Dense4Pattern))
                        self.view.scene().addItem(rect)
                        text = self.view.scene().addText(
                            key.split('-')[0],
                            QFont('monospace', 20, 400, False))
                        text.font().setPointSize(text.font().pointSize() * 2)
                        text.update()
                        text.setX(rect.rect().x() + 10)
                        text.setY(rect.rect().y() + 10)
                        print(f'set {key}')
	def generateComics( self, instance ):
		image = Image.new( mode="1", size=(0, 0) )
		for self.generatedComicNumber in range( self.numberOfComics ):
			try:
				if self.commandLineComicID is None:
					wordBubbleFileName = random.choice( os.listdir( self.wordBubblesDir ) )
				else:
					wordBubbleFileName = os.path.join( self.wordBubblesDir, self.commandLineComicID + ".tsv" )
			except IndexError as error:
				six.print_( error, file=sys.stderr )
				exit( EX_NOINPUT )
			
			if not self.silence:
				six.print_( "wordBubbleFileName:", wordBubbleFileName )
			
			if self.commandLineComicID is None:
				comicID = os.path.splitext( wordBubbleFileName )[ 0 ]
			else:
				comicID = self.commandLineComicID
			wordBubbleFileName = os.path.join( self.wordBubblesDir, wordBubbleFileName )
			if not self.silence:
				six.print_( "Loading word bubbles from", wordBubbleFileName )

			try:
				wordBubbleFile = open( wordBubbleFileName, mode="rt" )
			except OSError as error:
				six.print_( error, file=sys.stderr )
				exit( EX_NOINPUT )
			
			if not idChecker.checkFile( wordBubbleFile, wordBubbleFileName, self.commentMark ):
				six.print_( "Error: Word bubble file", wordBubbleFileName, "is not in the correct format." )
				exit( EX_DATAERR )
			
			lookForSpeakers = True
			speakers = []
			while lookForSpeakers:
				line = wordBubbleFile.readline()
				if len( line ) > 0:
					line = line.partition( self.commentMark )[0].strip()
					if len( line ) > 0:
						speakers = line.upper().split( "\t" )
						if len( speakers ) > 0:
							lookForSpeakers = False
				else:
					lookForSpeakers = False; #End of file reached, no speakers found
			
			if len( speakers ) == 0:
				six.print_( "Error: Word bubble file", wordBubbleFileName, "contains no speakers." )
				exit( EX_DATAERR )
			
			if not self.silence:
				six.print_( "These characters speak:", speakers )
			
			for speaker in speakers:
				if speaker not in self.generators:
					if not self.silence:
						six.print_( "Now building a Markov graph for character", speaker, "..." )
					newGenerator = Generator( charLabel = speaker, cm = self.commentMark, randomizeCapitals = self.randomizeCapitals )
					newGenerator.buildGraph( self.inDir )
					
					if not self.silence:
						newGenerator.showStats()
					
					self.generators[ speaker ] = newGenerator
			
			if not self.silence:
				six.print_( comicID )
			
			inImageFileName = os.path.join( self.imageDir, comicID + ".png" )
			
			try:
				image = Image.open( inImageFileName ).convert() #Text rendering looks better if we ensure the image's mode is not palette-based. Calling convert() with no mode argument does this.
			except IOError as error:
				six.print_( error, file=sys.stderr )
				exit( EX_NOINPUT )
			
			transcript = str( comicID ) + "\n"
			
			previousBox = ( int( -1 ), int( -1 ), int( -1 ), int( -1 ) ) #For detecting when two characters share a speech bubble; don't generate text twice.
			
			for line in wordBubbleFile:
				line = line.partition( self.commentMark )[ 0 ].strip()
				
				if len( line ) > 0:
					line = line.split( "\t" )
					character = line[ 0 ].rstrip( ":" ).strip().upper()
					
					try:
						generator = self.generators[ character ]
					except:
						six.print_( "Error: Word bubble file", wordBubbleFileName, "does not list", character, "in its list of speakers.", file=sys.stderr )
						exit( EX_DATAERR )
					
					topLeftX = int( line[ 1 ] )
					topLeftY = int( line[ 2 ] )
					bottomRightX = int( line[ 3 ] )
					bottomRightY = int( line[ 4 ] )
					
					box = ( topLeftX, topLeftY, bottomRightX, bottomRightY )
					
					if box != previousBox:
						previousBox = box
						
						text = ""
						nodeList = generator.generateSentences( 1 )[ 0 ]
						for node in nodeList:
							text += node.word + " "
						text.rstrip()
						
						oneCharacterTranscript = character + ": "
						oneCharacterTranscript += self.stringFromNodes( nodeList )
						if not self.silence:
							six.print_( oneCharacterTranscript )
						oneCharacterTranscript += "\n"
						transcript += oneCharacterTranscript
						
						wordBubble = image.crop( box )
						draw = ImageDraw.Draw( wordBubble )
						
						width = bottomRightX - topLeftX
						if width <= 0: #Width must be positive
							width = 1
						height = bottomRightY - topLeftY
						if height <= 0:
							height = 1
						
						size = int( height * 1.2 ) #Contrary to the claim by PIL's documentation, font sizes are apparently in pixels, not points. The size being requested is the height of a generic character; the actual height of any particular character will be approximately (not exactly) the requested size. We will try smaller and smaller sizes in the while loop below. The 1.2, used to account for the fact that real character sizes aren't exactly the same as the requested size, I just guessed an appropriate value.
						
						normalFont = ImageFont.truetype( self.normalFontFile, size = size )
						boldFont = ImageFont.truetype( self.boldFontFile, size = size )
						
						listoflists = self.rewrap_nodelistlist( nodeList, normalFont, boldFont, width, fontSize = size )
						
						margin = 0
						offset = originalOffset = 0
						goodSizeFound = False
						
						while not goodSizeFound:
							goodSizeFound = True
							totalHeight = 0
							for line in listoflists:
								
								lineWidth = 0
								lineHeight = 0
								for node in line:
									wordSize = normalFont.getsize( node.word + " " )
									lineWidth += wordSize[ 0 ]
									lineHeight = max( lineHeight, wordSize[ 1 ] )
								lineWidth -= normalFont.getsize( " " )[ 0 ]
								totalHeight += lineHeight
								if lineWidth > width:
									goodSizeFound = False
							
							if totalHeight > height:
								goodSizeFound = False
							
							if not goodSizeFound:
								size -= 1
								try:
									normalFont = ImageFont.truetype( self.normalFontFile, size = size )
									boldFont = ImageFont.truetype( self.boldFontFile, size = size )
								except IOError as error:
									six.print_( error, "\nUsing default font instead.", file=sys.stderr )
									normalFont = ImageFont.load_default()
									boldFont = ImageFont.load_default()
								listoflists = self.rewrap_nodelistlist( nodeList, normalFont, boldFont, width, fontSize = size )
						
						midX = int( wordBubble.size[ 0 ] / 2 )
						midY = int( wordBubble.size[ 1 ] / 2 )
						
						try: #Choose a text color that will be visible against the background
							backgroundColor = ImageStat.Stat( wordBubble ).mean #wordBubble.getpixel( ( midX, midY ) )
							textColorList = []
							
							useIntegers = False
							useFloats = False
							if wordBubble.mode.startswith( "1" ):
								bandMax = 1
								useIntegers = True
							elif wordBubble.mode.startswith( "L" ) or wordBubble.mode.startswith( "P" ) or wordBubble.mode.startswith( "RGB" ) or wordBubble.mode.startswith( "CMYK" ) or wordBubble.mode.startswith( "YCbCr" ) or wordBubble.mode.startswith( "LAB" ) or wordBubble.mode.startswith( "HSV" ):
								bandMax = 255
								useIntegers = True
							elif wordBubble.mode.startswith( "I" ):
								bandMax = 2147483647 #max for a 32-bit signed integer
								useIntegers = True
							elif wordBubble.mode.startswith( "F" ):
								bandMax = float( "infinity" )
								useFloats = True
							else: #I've added all modes currently supported according to Pillow documentation; this is for future compatibility
								bandMax = max( ImageStat.Stat( image ).extrema )
							
							for c in backgroundColor:
								d = bandMax - ( c * 1.5 )
								
								if d < 0:
									d = 0
								
								if useIntegers:
									d = int( d )
								elif useFloats:
									d = float( d )
								
								textColorList.append( d )
							
							if wordBubble.mode.endswith( "A" ): #Pillow supports two modes with alpha channels
								textColorList[ -1 ] = bandMax
							
							textColor = tuple( textColorList )
							
						except ValueError:
							textColor = "black"
						
						offset = originalOffset
						for line in listoflists:
							xOffset = 0
							yOffsetAdditional = 0
							for node in line:
								usedFont = node.font
								draw.text( ( margin + xOffset, offset ), node.word + " ", font = usedFont, fill = textColor )
								tempSize = usedFont.getsize( node.word + " " )
								xOffset += tempSize[ 0 ]
								yOffsetAdditional = max( yOffsetAdditional, tempSize[ 1 ] )
								node.unselectStyle()
							offset += yOffsetAdditional
						
						image.paste( wordBubble, box )
						
			wordBubbleFile.close()
			
			if self.numberOfComics > 1:
				oldOutTextFileName = self.outTextFileName
				temp = os.path.splitext(self.outTextFileName )
				self.outTextFileName = temp[ 0 ] + str( self.generatedComicNumber ) + temp[ 1 ]
			
			#---------------------------Split into separate function
			try:
				#os.makedirs( os.path.dirname( outTextFileName ), exist_ok = True )
				outFile = open( self.outTextFileName, mode="wt" )
			except OSError as error:
				six.print_( error, "\nUsing standard output instead", file=sys.stderr )
				outFile = sys.stdout
			
			if self.numberOfComics > 1:
				self.outTextFileName = oldOutTextFileName
			
			six.print_( transcript, file=outFile )
			
			outFile.close()
			
			if self.numberOfComics > 1:
				oldOutImageFileName = self.outImageFileName
				temp = os.path.splitext( self.outImageFileName )
				outImageFileName = temp[ 0 ] + str( self.generatedComicNumber ) + temp[ 1 ]
			
			if self.topImageFileName != None:
				try:
					topImage = Image.open( self.topImageFileName ).convert( mode=image.mode )
				except IOError as error:
					six.print_( error, file=sys.stderr )
					exit( EX_NOINPUT )
				oldSize = topImage.size
				size = ( max( topImage.size[ 0 ], image.size[ 0 ] ), topImage.size[ 1 ] + image.size[ 1 ] )
				
				newImage = Image.new( mode=image.mode, size=size )
				newImage.paste( im=topImage, box=( 0, 0 ) )
				newImage.paste( im=image, box=( 0, oldSize[ 1 ] ) )
				image = newImage
			
			
			
			originalURL = None
			URLFile = open( os.path.join( self.inDir, "sources.tsv" ), "rt" )
			for line in URLFile:
				line = line.partition( self.commentMark )[ 0 ].strip()
				
				if len( line ) > 0:
					line = line.split( "\t" )
					
					if comicID == line[ 0 ]:
						originalURL = line[ 1 ]
						break;
			URLFile.close()
			
			transcriptWithURL = transcript + "\n" + originalURL #The transcript that gets embedded into the image file should include the URL. The transcript that gets uploaded to blogs doesn't need it, as the URL gets sent anyway.
			
			infoToSave = PngInfo()
			
			encodingErrors = "backslashreplace" #If we encounter errors during text encoding, I feel it best to replace unencodable text with escape sequences; that way it may be possible for reader programs to recover the original unencodable text.
			
			#According to the Pillow documentation, key names should be "latin-1 encodable". I take this to mean that we ourselves don't need to encode it in latin-1.
			key = "transcript"
			keyUTF8 = key.encode( "utf-8", errors=encodingErrors )
			
			#uncomment the following if using Python 3
			#transcriptISO = transcriptWithURL.encode( "iso-8859-1", errors=encodingErrors )
			#transcriptUTF8 = transcriptWithURL.encode( "utf-8", errors=encodingErrors )
			
			#python 2:
			tempencode = transcriptWithURL.decode( 'ascii', errors='replace' ) # I really don't like using this ascii-encoded intermediary called tempencode, but i couldn't get the program to work when encoding directly to latin-1
			transcriptISO = tempencode.encode( "iso-8859-1", errors='replace' )
			transcriptUTF8 = tempencode.encode( "utf-8", errors='replace' )
			
			
			infoToSave.add_itxt( key=key, value=transcriptUTF8, tkey=keyUTF8 )
			infoToSave.add_text( key=key, value=transcriptISO )
			
			#GIMP only recognizes comments
			key = "Comment"
			keyUTF8 = key.encode( "utf-8", errors=encodingErrors )
			
			infoToSave.add_text( key=key, value=transcriptISO )
			infoToSave.add_itxt( key=key, value=transcriptUTF8, tkey=keyUTF8 )
			
			try:
				#os.makedirs( os.path.dirname( outImageFileName ), exist_ok = True )
				if self.saveForWeb:
					image = image.convert( mode = "P", palette="ADAPTIVE", dither=False ) #Try turning dithering on or off.
					image.save( self.outImageFileName, format="PNG", optimize=True, pnginfo=infoToSave )
				else:
					image.save( self.outImageFileName, format="PNG", pnginfo=infoToSave )
			except IOError as error:
				six.print_( error, file = sys.stderr )
				exit( EX_CANTCREAT )
			except OSError as error:
				six.print_( error, file = sys.stderr )
				exit( EX_CANTCREAT )
			
			if not self.silence:
				six.print_( "Original comic URL:", originalURL )
			
			for blog in self.blogUploaders:
				blog.upload( postStatus = "publish", inputFileName = outImageFileName, shortComicTitle = self.shortName, longComicTitle = self.longName, transcript = transcript, originalURL = originalURL, silence = self.silence )
			
			if self.numberOfComics > 1:
				outImageFileName = oldOutImageFileName
		#end of loop: for generatedComicNumber in range( numberOfComics ):
		
		#---------------------------It's display time!
		if image.mode != "RGB":
			image = image.convert( mode = "RGB" )
		self.gui.comicArea.texture = Texture.create( size = image.size, colorfmt = 'rgb' )
		self.gui.comicArea.texture.blit_buffer( pbuffer = image.transpose( Image.FLIP_TOP_BOTTOM ).tobytes(), colorfmt = 'rgb' )