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.
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.