How to copy an image from qtextedit in pyside6 and paste it into MS-WORD

100 Views Asked by At

I'm using pyside6's qtextedit to write text and images. And in this state, I want to copy the content to the clipboard and insert it into ms-word.

The content of qtextedit consists of several texts and images.

pyside6 qtextedit content

I created qtextedit content as above. And the above is a photo captured using Windows Capture.

Content of MS-word

The photo above is the content I Ctrl+C pasted into MS-word. As shown above, the images are not visible at all.

aosdijfioas

   asiufisuh

The above content is the same as ctrl+c in qtextedit and pasted into stackoverflow with ctrl+v. "" .

I don't know what this is, but maybe I need to do something when inserting an image into qtextedit or copying it with ctrl+c. How can I use it in another editor?

Currently, qtextedit is in its default state created by attaching it from pyside-designer.

Honestly, I don't know what to do because I didn't expect this to happen. I used Ctrl+C, but I didn't know that the image wouldn't be copied.

1

There are 1 best solutions below

0
musicamante On

Rich text content, similarly to HTML, normally uses references for embedded objects like images: the object is not physically stored within the document, but only a reference (usually, a local or remote address) to it.

This is also reflected when using the OS clipboard, and for good reasons: imagine copying a text selection that contains dozens of images that are just references to local files: image data takes a lot of memory, and it's quite easy to risk "out-of-memory" issues if you try to copy them.

Qt doesn't provide a direct way to do this, since it's a potentially harmful procedure; still, the requirement is valid and acceptable.

Since cross-platform clipboard operations of rich text is normally done using HTML, the solution is to use the Base64 encoding that HTML also supports: image data can be stored using standard ASCII characters that are used to encode the binary data of the image. In that case, the images are not references anymore: they are embedded within the document as raw data and their address is a memory representation of the file data.

The trick is to properly manage the operation while copying the selection to the clipboard.

Luckily, QTextEdit provides the createMimeDataFromSelection() function: since it's "virtual", it means that it can be overridden in Python and will always be called when the user attempts to create a copyable selection.

Consider that QTextDocument uses a special subclass to display images: QTextImageFormat (which inherits QTextCharFormat and QTextFormat) is virtually a "character" in the document, that is eventually "converted" to a displayed image.

The process uses the following steps:

  • create a clone of the current QTextDocument, in order to properly copy its basic aspects;
  • set its HTML only based on the current selection (it would use the toHtml() function anyway, so that conversion is not an issue);
  • iterate through the whole "selection document" and its fragments (each fragment corresponds to a different QTextFormat, including those used for images);
  • if an image text format is found, check if it refers to a locally stored image (the name() of QTextImageFormat or the ImageName property of an uncast QTextCharFormat is similar to the src="..." attribute of an HTML <img> tag) and if it actually is a valid image;
  • if it does, use base64 to encode the data of the image and alter the name property to reflect that, which is what QTextDocument does to retrieve and display images;
  • create a QMimeData object with the related plain text and rich text (including the "embedded" images), then return it;

In reality, while the above seems very complex, it can be achieved quite easily, as long as one knows what to do:

class Editor(QTextEdit):
    def _updateImageFormat(self, fmt):
        '''
        A helper function that receives a QTextCharFormat.
        If it is a QTextImageFormat and its "name" refers to a local file, then
        it changes that "name" (originally, the path) to a base64 encoded data 
        of the locally linked image.
        Returns True if the image has been encoded (thus requiring an update
        on the QTextCursor) or False for any other case.
        '''
        if fmt.isImageFormat():
            path = fmt.property(fmt.Property.ImageName)
            if QUrl(path).scheme() != 'data':
                if QUrl(path).isLocalFile():
                    path = QUrl(path).toLocalFile()
                image = QImage(path)
                if not image.isNull():
                    binary = base64.b64encode(open(path, 'rb').read())
                    fmt.setProperty(fmt.Property.ImageName, 
                        'data:image/*;base64,' + str(binary, 'utf-8'))
                    return True
        return False

    def createMimeDataFromSelection(self):
        doc = self.document().clone()
        doc.setHtml(self.textCursor().selection().toHtml())
        newCursor = QTextCursor(doc)
        block = doc.begin()
        while block.isValid():
            it = block.begin()
            while not it.atEnd():
                fragment = it.fragment()
                fmt = fragment.charFormat()
                if self._updateImageFormat(fmt): # the format must be updated
                    newCursor.setPosition(fragment.position())
                    newCursor.setPosition(
                        fragment.position() + fragment.length(), 
                        newCursor.MoveMode.KeepAnchor
                    )
                    newCursor.setCharFormat(fmt)
                
                it += 1

            block = block.next()

        mime = QMimeData()
        mime.setText(doc.toPlainText())
        mime.setHtml(doc.toHtml(QByteArray(b'utf-8')))

        return mime

Now, remember the memory issue above. Even if modern computers normally have enough RAM, converting lots of huge images into ASCII encoded strings is not always a good idea.

The above code works quite fine for very simple cases (a few, relatively small images), but it could create issues for big data sizes, like lots of images or uncompressed ones. So, use it with care, and eventually consider some failsafe system, like basic alerts or pre-processing file sizes before actually proceeding with clipboard copying.

Finally, be aware that all this will only work for relatively modern programs that properly support HTML based clipboard and embedded images: rich text clipboard is not an absolute certainty, and it may not work as expected, so you must be aware of that and also consider to eventually alert the user about the consequences. Conversion to base64 always requires more memory as opposed to the original data size, and support by other programs cannot be taken as granted.
Also, using clipboard within the same QTextEdit as done above will make any copy/pasted image as embedded, even if done within the same widget, so you should consider some further checking, possibly by creating an internal "clipboard system" that would eventually paste real QTextDocument fragments instead of the HTML provided by the overall clipboard.