import sys from PyQt5 import QtWidgets as qtw from PyQt5 import QtGui as qtg from PyQt5 import QtCore as qtc class ColorButton(qtw.QPushButton): changed = qtc.pyqtSignal() def __init__(self, default_color, changed=None): super().__init__() self.set_color(qtg.QColor(default_color)) self.clicked.connect(self.on_click) if changed: self.changed.connect(changed) def set_color(self, color): self._color = color # update icon pixmap = qtg.QPixmap(32, 32) pixmap.fill(self._color) self.setIcon(qtg.QIcon(pixmap)) def on_click(self): color = qtw.QColorDialog.getColor(self._color) if color: self.set_color(color) self.changed.emit() class FontButton(qtw.QPushButton): changed = qtc.pyqtSignal() def __init__(self, default_family, default_size, changed=None): super().__init__() self.set_font(qtg.QFont(default_family, default_size)) self.clicked.connect(self.on_click) if changed: self.changed.connect(changed) def set_font(self, font): self._font = font self.setFont(font) self.setText(f'{font.family()} {font.pointSize()}') def on_click(self): font, accepted = qtw.QFontDialog.getFont(self._font) if accepted: self.set_font(font) self.changed.emit() class ImageFileButton(qtw.QPushButton): changed = qtc.pyqtSignal() def __init__(self, changed=None): super().__init__("Click to select…") self._filename = None self.clicked.connect(self.on_click) if changed: self.changed.connect(changed) def on_click(self): filename, _ = qtw.QFileDialog.getOpenFileName( None, "Select an image to use", qtc.QDir.homePath(), "Images (*.png *.xpm *.jpg)") if filename: self._filename = filename # set button text to filename without path self.setText(qtc.QFileInfo(filename).fileName()) self.changed.emit() class MemeEditForm(qtw.QWidget): changed = qtc.pyqtSignal(dict) def __init__(self): super().__init__() self.setLayout(qtw.QFormLayout()) # Image self.image_source = ImageFileButton(changed=self.on_change) self.layout().addRow('Image file', self.image_source) # Text entries self.top_text = qtw.QPlainTextEdit(textChanged=self.on_change) self.bottom_text = qtw.QPlainTextEdit(textChanged=self.on_change) self.layout().addRow("Top Text", self.top_text) self.layout().addRow("Bottom Text", self.bottom_text) # Text color and font self.text_color = ColorButton('white', changed=self.on_change) self.layout().addRow("Text Color", self.text_color) self.text_font = FontButton('Impact', 32, changed=self.on_change) self.layout().addRow("Text Font", self.text_font) # Background Boxes self.text_bg_color = ColorButton('black', changed=self.on_change) self.layout().addRow('Text Background', self.text_bg_color) self.top_bg_height = qtw.QSpinBox( minimum=0, maximum=32, valueChanged=self.on_change, suffix=' line(s)') self.layout().addRow('Top BG height', self.top_bg_height) self.bottom_bg_height = qtw.QSpinBox( minimum=0, maximum=32, valueChanged=self.on_change, suffix=' line(s)') self.layout().addRow('Bottom BG height', self.bottom_bg_height) self.bg_padding = qtw.QSpinBox( minimum=0, maximum=100, value=10, valueChanged=self.on_change, suffix=' px') self.layout().addRow('BG Padding', self.bg_padding) # Deep Fryer self.deep_fry = qtw.QCheckBox('Deep Fry', stateChanged=self.on_change) self.layout().addRow(self.deep_fry) def on_change(self): data = { 'top_text': self.top_text.toPlainText(), 'bottom_text': self.bottom_text.toPlainText(), 'text_color': self.text_color._color, 'text_font': self.text_font._font, 'bg_color': self.text_bg_color._color, 'top_bg_height': self.top_bg_height.value(), 'bottom_bg_height': self.bottom_bg_height.value(), 'bg_padding': self.bg_padding.value(), 'deep_fry': self.deep_fry.isChecked() } self.changed.emit(data) class MainWindow(qtw.QMainWindow): def __init__(self): """MainWindow constructor. This widget will be our main window. We'll define all the UI components in here. """ super().__init__() # Main UI code goes here self.setWindowTitle('Qt Meme Generator') # define some constants self.max_size = qtc.QSize(800, 600) self.image = qtg.QImage( self.max_size, qtg.QImage.Format_ARGB32) self.image.fill(qtg.QColor('black')) # Container widget mainwidget = qtw.QWidget() self.setCentralWidget(mainwidget) mainwidget.setLayout(qtw.QHBoxLayout()) # Image Previewer self.image_display = qtw.QLabel(pixmap=qtg.QPixmap(self.image)) mainwidget.layout().addWidget(self.image_display) # The editing form self.form = MemeEditForm() mainwidget.layout().addWidget(self.form) self.form.changed.connect(self.build_image) # Create file loading and saving toolbar = self.addToolBar('File') toolbar.addAction("Save Image", self.save_image) # End main UI code self.show() def save_image(self): save_file, _ = qtw.QFileDialog.getSaveFileName( None, "Save your image", qtc.QDir.homePath(), "PNG Images (*.png)") if save_file: self.image.save(save_file, "PNG") def build_image(self, data): # Create a QImage file if not data.get('image_source'): self.image.fill(qtg.QColor('black')) else: self.image.load(data.get('image_source')) # Scale down the image if it's over the max_size if not (self.max_size - self.image.size()).isValid(): # isValid returns false if either dimension is negative self.image = self.image.scaled( self.max_size, qtc.Qt.KeepAspectRatio) # create the painter painter = qtg.QPainter(self.image) # Paint the background blocks font_px = qtg.QFontInfo(data['text_font']).pixelSize() top_px = (data['top_bg_height'] * font_px) + data['bg_padding'] top_block_rect = qtc.QRect( 0, 0, self.image.width(), top_px) bottom_px = ( self.image.height() - data['bg_padding'] - (data['bottom_bg_height'] * font_px)) bottom_block_rect = qtc.QRect( 0, bottom_px, self.image.width(), self.image.height()) painter.setBrush(qtg.QBrush(data['bg_color'])) painter.drawRect(top_block_rect) painter.drawRect(bottom_block_rect) # Paint the text painter.setPen(data['text_color']) painter.setFont(data['text_font']) flags = qtc.Qt.AlignHCenter | qtc.Qt.TextWordWrap painter.drawText( self.image.rect(), flags | qtc.Qt.AlignTop, data['top_text']) painter.drawText( self.image.rect(), flags | qtc.Qt.AlignBottom, data['bottom_text']) # Deep fry if data['deep_fry']: # To do image-level editing, # we need to explicitly end our painting first painter.end() # Now we can do things that overwrite the image # Swapping Red and Blue self.image = self.image.rgbSwapped() # Convert to a 8-bit color self.image = self.image.convertToFormat(qtg.QImage.Format_Indexed8) # We're going to grab all the colors from the color table # then alter the hue and saturation of each color colors = self.image.colorTable() new_colors = [] for color in colors: qcolor = qtg.QColor(color) qcolor.setHslF( ((qcolor.hueF() * 200) % 100)/100, min(1, qcolor.saturationF() * 2), qcolor.lightnessF()) new_colors.append(qcolor.rgba()) self.image.setColorTable(new_colors) # Convert back to the original format self.image = self.image.convertToFormat(qtg.QImage.Format_ARGB32) # show the image self.image_display.setPixmap(qtg.QPixmap(self.image)) if __name__ == '__main__': app = qtw.QApplication(sys.argv) # it's required to save a reference to MainWindow. # if it goes out of scope, it will be destroyed. mw = MainWindow() sys.exit(app.exec())