Search code examples
pythonpyqt5copypasteqtextedit

PyQt5: Copy an Image from QTextEdit and paste into paint


I'm trying to allow my text edit to copy and paste an image from QTextEdit to Microsoft Paint. Currently I have a custom text edit that allows me to paste images into the text edit. I save the entire document as HTML and reload it later so when I paste the images I use <img src= \"data:image/*;base64," + binary + "\" max-width=100% max-height=100%/> where the binary is a resized base64 binary image. This way the image binary is embedded into the HTML and I don't need to keep track of any source images.

Here is the code for my QTextEdit that allows for that paste.

class TextEdit(QTextEdit):
    def __init__(self):
       super().__init__()
       self.setMaximumWidth(800)
       self.setStyleSheet("background-color: rgb(255, 255, 255);")
    
    def canInsertFromMimeData(self, source):

        if source.hasImage():
            return True
        else:
            return super(TextEdit, self).canInsertFromMimeData(source)

    def insertFromMimeData(self, source):

        cursor = self.textCursor()
        document = self.document()

        if source.hasUrls():

            for u in source.urls():
                file_ext = os.path.splitext(str(u.toLocalFile()))[1].lower()
                if u.isLocalFile() and file_ext in IMAGE_EXTENSIONS:
                    
                    img = Image.open(u.toLocalFile())
                    width, height = img.size

                    max_width  = int(document.pageSize().width())
                    max_height = int(max_width * height / width)
                    
                    if width > max_width or height > max_height:
                        img = img.resize((max_width, max_height), Image.ANTIALIAS)

                    output = io.BytesIO()
                    img.save(output, format=file_ext.replace(".",""), quality=3000)
                    binary = str(base64.b64encode(output.getvalue()))
                    binary = binary.replace("b'", "")
                    binary = binary.replace("'", "")
                    
                    HTMLBin = "<img src= \"data:image/*;base64," + binary + "\" max-width=100% max-height=100%/>"
                    cursor.insertHtml(HTMLBin)                   
                    
                    return

                else:
                    # If we hit a non-image or non-local URL break the loop and fall out
                    # to the super call & let Qt handle it
                    break

            else:
                # If all were valid images, finish here.
                return
            
        elif source.hasImage():
            img = source.imageData()
            
            #Save the image from the clipboard into a buffer
            ba = QtCore.QByteArray()
            buffer = QtCore.QBuffer(ba)
            buffer.open(QtCore.QIODevice.WriteOnly)
            img.save(buffer, 'PNG')
          
            #Load the image binary into PIL.Image (Python Image Library)
            img = Image.open(io.BytesIO(ba.data()))
            
            #Find what the max size of the document is.
            width, height = img.size
            
            max_width = 775
            max_height = int(max_width * height / width)
            
            #Resize to make sure the image fits in the document.
            if (width > max_width or height > max_height) and (max_width!=0 and max_height!=0):
                img = img.resize((max_width, max_height), Image.ANTIALIAS)

            #Convert binary into a string representation of the base64 to embed the image into the HTML
            output = io.BytesIO()
            img.save(output, format='PNG', quality=95)
            binary = str(base64.b64encode(output.getvalue()))
            
            # base64_data = ba.toBase64().data()
            # binary = str(base64_data)
            binary = binary.replace("b'", "")
            binary = binary.replace("'", "")
            
            #Write the HTML and insert it at the Document Cursor.
            HTMLBin = "<img src= \"data:image/*;base64," + binary + "\" max-width=100% max-height=100%> </img>"
            cursor.insertHtml(HTMLBin)
            return
            
        elif source.hasHtml():
            html = source.html()
            cursor.insertHtml(html)
            return

        super(TextEdit, self).insertFromMimeData(source)

Now what I need to do is when the image is copied again, I basically need to remove the HTML img tag around the binary and copy just the binary.

I tried to overload the QTextEdit.copy() method which didn't seem to do anything, and I tried to overwrite QTextEdit.createMimeDataFromSelection() but I wasn't able to find a way to get the copy to work the same as before once I did that.

Does anyone have any information on how to accomplish this? I'd like the solution to be within my custom QTextEdit and not be done outside of that class just so I can re-use this same functionality elsewhere with other projects.


Solution

  • In order to copy an image to the clipboard, you have to first ensure that the selection only contains one image and nothing else: no text and no other images.

    Considering that images occupy only one character in the text document (used as a "placeholder" for the image object), you can check if the length of the selection is actually 1, and then verify that the character format at the end of the selection is a QTextImageFormat, which is a special type of QTextFormat used for images; the position at the end of the selection is fundamental, as formats are always considered at the left of the cursor.

    By overriding createMimeDataFromSelection and doing the above you can easily return a QMimeData that contains the image loaded from the document's resources.

    Note that a better (and simpler) approach to add images from the clipboard is to use the existing Qt features, so that you can avoid an unnecessary conversion through PIL and passing through two data buffers.

    IMAGE_EXTENSIONS = [
        str(f, 'utf-8') for f in QtGui.QImageReader.supportedImageFormats()]
    
    class TextEdit(QtWidgets.QTextEdit):
        def __init__(self):
            super().__init__()
            self.setMaximumWidth(800)
            self.setStyleSheet("QTextEdit { background-color: rgb(255, 255, 255); }")
    
        def canInsertFromMimeData(self, source):
            if source.hasImage():
                return True
            else:
                return super(TextEdit, self).canInsertFromMimeData(source)
    
        def createMimeDataFromSelection(self):
            cursor = self.textCursor()
            if len(cursor.selectedText()) == 1:
                cursor.setPosition(cursor.selectionEnd())
                fmt = cursor.charFormat()
                if fmt.isImageFormat():
                    url = QtCore.QUrl(fmt.property(fmt.ImageName))
                    image = self.document().resource(
                        QtGui.QTextDocument.ImageResource, url)
                    mime = QtCore.QMimeData()
                    mime.setImageData(image)
                    return mime
            return super().createMimeDataFromSelection()
    
        def insertImage(self, image):
            if image.isNull():
                return False
            if isinstance(image, QtGui.QPixmap):
                image = image.toImage()
    
            doc = self.document()
            if image.width() > doc.pageSize().width():
                image = image.scaledToWidth(int(doc.pageSize().width()), 
                    QtCore.Qt.SmoothTransformation)
    
            ba = QtCore.QByteArray()
            buffer = QtCore.QBuffer(ba)
            image.save(buffer, 'PNG', quality=95)
            binary = base64.b64encode(ba.data())
            HTMLBin = "<img src= \"data:image/*;base64,{}\" max-width=100% max-height=100%></img>".format(
                str(binary, 'utf-8'))
            self.textCursor().insertHtml(HTMLBin)
    
            return True
    
        def insertFromMimeData(self, source):
            if source.hasImage() and self.insertImage(source.imageData()):
                return
            elif source.hasUrls():
                for url in source.urls():
                    if not url.isLocalFile():
                        continue
                    path = url.toLocalFile()
                    info = QtCore.QFileInfo(path)
                    if not info.suffix().lower() in IMAGE_EXTENSIONS:
                        continue
                    elif self.insertImage(QtGui.QImage(path)):
                        return
            super().insertFromMimeData(source)
    

    Note that I changed the stylesheet with a proper class selector: this is very important as setting generic properties for complex widgets (most importantly, scroll areas) can create graphical issues, as they are propagated to all children of that widget, including scroll bars (which become ugly with certain styles), context menus and modal dialogs.