<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>Python GUIs - mime-data</title><link href="https://www.pythonguis.com/" rel="alternate"/><link href="https://www.pythonguis.com/feeds/mime-data.tag.atom.xml" rel="self"/><id>https://www.pythonguis.com/</id><updated>2020-05-11T09:00:00+00:00</updated><subtitle>Create GUI applications with Python and Qt</subtitle><entry><title>Handling Image Drag and Drop from Web Browsers in PyQt6 — Why toLocalFile() returns an empty string and how to handle remote image drops correctly</title><link href="https://www.pythonguis.com/faq/dragging-and-dropping-in-rich-text-example-app/" rel="alternate"/><published>2020-05-11T09:00:00+00:00</published><updated>2020-05-11T09:00:00+00:00</updated><author><name>Martin Fitzpatrick</name></author><id>tag:www.pythonguis.com,2020-05-11:/faq/dragging-and-dropping-in-rich-text-example-app/</id><summary type="html">When dragging and dropping images from a web browser into a PyQt6 rich text editor, &lt;code&gt;toLocalFile()&lt;/code&gt; sometimes returns a blank string. It works for some images (like Google image search results) but fails for others (like images embedded directly on a webpage). Why does this happen, and how can I handle it?</summary><content type="html">
            &lt;blockquote&gt;
&lt;p&gt;When dragging and dropping images from a web browser into a PyQt6 rich text editor, &lt;code&gt;toLocalFile()&lt;/code&gt; sometimes returns a blank string. It works for some images (like Google image search results) but fails for others (like images embedded directly on a webpage). Why does this happen, and how can I handle it?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If you've added drag and drop support to your application, you may have noticed something frustrating: dragging an image from a browser sometimes works perfectly, and other times gives you nothing at all. The &lt;code&gt;toLocalFile()&lt;/code&gt; method returns an empty string, and your image never appears.&lt;/p&gt;
&lt;p&gt;This comes down to how browsers package image data when you start a drag operation, and what your application expects to receive. Let's walk through what's happening and how to fix it.&lt;/p&gt;
&lt;h2 id="how-drag-and-drop-mime-data-works"&gt;How drag and drop MIME data works&lt;/h2&gt;
&lt;p&gt;When you drag something &amp;mdash; a file, an image, some text &amp;mdash; the source application bundles that data into a &lt;code&gt;QMimeData&lt;/code&gt; object. This object can contain several different formats at once. For example, dragging an image might include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A file URL (&lt;code&gt;text/uri-list&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Raw image data (&lt;code&gt;image/png&lt;/code&gt; or &lt;code&gt;image/jpeg&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;An HTML &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tag (&lt;code&gt;text/html&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;A plain text URL (&lt;code&gt;text/plain&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Which of these formats are included depends entirely on the source application. Different browsers, and even different types of images within the same browser, behave differently.&lt;/p&gt;
&lt;h2 id="why-tolocalfile-returns-an-empty-string"&gt;Why toLocalFile() returns an empty string&lt;/h2&gt;
&lt;p&gt;The method &lt;code&gt;QUrl.toLocalFile()&lt;/code&gt; converts a URL into a local filesystem path. It only works when the URL uses the &lt;code&gt;file://&lt;/code&gt; scheme &amp;mdash; meaning the file actually exists on your computer.&lt;/p&gt;
&lt;p&gt;When you drag an image from a Google image search, the browser often creates a temporary local file and provides a &lt;code&gt;file://&lt;/code&gt; URL. That's why &lt;code&gt;toLocalFile()&lt;/code&gt; works in that case.&lt;/p&gt;
&lt;p&gt;But when you drag an image that's embedded directly in a webpage (like a screenshot in a blog post), the browser typically provides a remote &lt;code&gt;http://&lt;/code&gt; or &lt;code&gt;https://&lt;/code&gt; URL instead. There's no local file, so &lt;code&gt;toLocalFile()&lt;/code&gt; returns an empty string. Some browsers may also provide the image as inline data or an HTML fragment with no URL at all.&lt;/p&gt;
&lt;h2 id="inspecting-what-the-browser-actually-sends"&gt;Inspecting what the browser actually sends&lt;/h2&gt;
&lt;p&gt;A good first step is to look at exactly what MIME data arrives when you drop something. This small example creates a drop target that prints out all available MIME formats and their contents:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-python"&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class="python"&gt;import sys

from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QApplication, QLabel, QVBoxLayout, QWidget


class DropInspector(QLabel):
    def __init__(self):
        super().__init__("Drop something here")
        self.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.setMinimumSize(400, 300)
        self.setStyleSheet(
            "background-color: #f0f0f0; border: 2px dashed #aaa; font-size: 16px;"
        )
        self.setAcceptDrops(True)

    def dragEnterEvent(self, event):
        event.acceptProposedAction()

    def dropEvent(self, event):
        mime_data = event.mimeData()
        print("=== Drop received ===")
        for fmt in mime_data.formats():
            data = mime_data.data(fmt)
            print(f"\nFormat: {fmt}")
            # Show first 200 bytes as text for readability.
            try:
                print(f"  Data: {bytes(data[:200]).decode('utf-8', errors='replace')}")
            except Exception:
                print(f"  Data: ({len(data)} bytes, binary)")

        if mime_data.hasUrls():
            for url in mime_data.urls():
                print(f"\nURL: {url.toString()}")
                print(f"  toLocalFile: '{url.toLocalFile()}'")
                print(f"  scheme: '{url.scheme()}'")

        event.acceptProposedAction()
        self.setText("Check console output!")


app = QApplication(sys.argv)
window = DropInspector()
window.show()
sys.exit(app.exec())
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;Try dragging different images from your browser into this window. You'll see that some drops include &lt;code&gt;file://&lt;/code&gt; URLs while others include &lt;code&gt;https://&lt;/code&gt; URLs or even raw image data with no URL at all.&lt;/p&gt;
&lt;h2 id="handling-all-cases-in-your-drop-event"&gt;Handling all cases in your drop event&lt;/h2&gt;
&lt;p&gt;To make your application work reliably with images dragged from any source, you need to handle multiple scenarios:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Local file URL&lt;/strong&gt; &amp;mdash; use the file path directly&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Remote URL&lt;/strong&gt; &amp;mdash; download the image&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Raw image data&lt;/strong&gt; &amp;mdash; use it directly from the MIME data&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HTML with an &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tag&lt;/strong&gt; &amp;mdash; extract the image URL from the HTML&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here's how to implement this step by step.&lt;/p&gt;
&lt;h3&gt;Checking for local files first&lt;/h3&gt;
&lt;p&gt;This is the simplest case and the one you likely already have working:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-python"&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class="python"&gt;def dropEvent(self, event):
    mime_data = event.mimeData()

    if mime_data.hasUrls():
        for url in mime_data.urls():
            local_path = url.toLocalFile()
            if local_path:
                # It's a local file &amp;mdash; use it directly.
                self.insert_image_from_path(local_path)
                event.acceptProposedAction()
                return
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3&gt;Handling remote URLs&lt;/h3&gt;
&lt;p&gt;When &lt;code&gt;toLocalFile()&lt;/code&gt; returns an empty string but you still have a URL, it's likely a remote image. You can download it using Python's &lt;code&gt;urllib&lt;/code&gt; (or &lt;code&gt;requests&lt;/code&gt; if you prefer):&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-python"&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class="python"&gt;import os
import tempfile
import urllib.request


def download_image(url_string):
    """Download an image from a URL and return the local file path."""
    try:
        # Create a temporary file to store the downloaded image.
        suffix = os.path.splitext(url_string)[-1].split("?")[0]
        if suffix not in (".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp"):
            suffix = ".png"
        tmp_file = tempfile.NamedTemporaryFile(delete=False, suffix=suffix)
        urllib.request.urlretrieve(url_string, tmp_file.name)
        return tmp_file.name
    except Exception as e:
        print(f"Failed to download image: {e}")
        return None
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;Then extend your drop handler:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-python"&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class="python"&gt;if mime_data.hasUrls():
    for url in mime_data.urls():
        local_path = url.toLocalFile()
        if local_path:
            self.insert_image_from_path(local_path)
            event.acceptProposedAction()
            return

        # No local file &amp;mdash; try downloading the remote URL.
        url_string = url.toString()
        if url_string:
            local_path = download_image(url_string)
            if local_path:
                self.insert_image_from_path(local_path)
                event.acceptProposedAction()
                return
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3&gt;Handling raw image data&lt;/h3&gt;
&lt;p&gt;Sometimes the browser sends the image data directly, without any URL. You can check for this using &lt;code&gt;hasImage()&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-python"&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class="python"&gt;if mime_data.hasImage():
    image = mime_data.imageData()
    if image and not image.isNull():
        # Save the image to a temp file and insert it.
        tmp_path = tempfile.NamedTemporaryFile(
            delete=False, suffix=".png"
        ).name
        image.save(tmp_path)
        self.insert_image_from_path(tmp_path)
        event.acceptProposedAction()
        return
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3&gt;Extracting URLs from HTML&lt;/h3&gt;
&lt;p&gt;As a fallback, some drops include an HTML fragment with an &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tag. You can parse out the &lt;code&gt;src&lt;/code&gt; attribute:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-python"&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class="python"&gt;import re


def extract_image_url_from_html(html):
    """Extract the first image URL from an HTML string."""
    match = re.search(r'&amp;lt;img[^&amp;gt;]+src=["\']([^"\']+)["\']', html)
    if match:
        return match.group(1)
    return None
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;Then add this as a final fallback:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-python"&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class="python"&gt;if mime_data.hasHtml():
    html = mime_data.html()
    image_url = extract_image_url_from_html(html)
    if image_url:
        local_path = download_image(image_url)
        if local_path:
            self.insert_image_from_path(local_path)
            event.acceptProposedAction()
            return
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 id="complete-working-example"&gt;Complete working example&lt;/h2&gt;
&lt;p&gt;Here's a full, working rich text editor with robust image drag and drop support. You can copy this and run it directly:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-python"&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class="python"&gt;import os
import re
import sys
import tempfile
import urllib.request

from PyQt6.QtCore import Qt
from PyQt6.QtGui import QImage, QTextCursor
from PyQt6.QtWidgets import QApplication, QMainWindow, QTextEdit, QVBoxLayout, QWidget


def download_image(url_string):
    """Download an image from a URL and return the local file path."""
    try:
        suffix = os.path.splitext(url_string.split("?")[0])[-1]
        if suffix.lower() not in (".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp"):
            suffix = ".png"
        tmp_file = tempfile.NamedTemporaryFile(delete=False, suffix=suffix)
        urllib.request.urlretrieve(url_string, tmp_file.name)
        return tmp_file.name
    except Exception as e:
        print(f"Failed to download image: {e}")
        return None


def extract_image_url_from_html(html):
    """Extract the first image URL from an HTML string."""
    match = re.search(r'&amp;lt;img[^&amp;gt;]+src=["\']([^"\']+)["\']', html)
    if match:
        return match.group(1)
    return None


class ImageDropTextEdit(QTextEdit):
    def __init__(self):
        super().__init__()
        self.setAcceptDrops(True)

    def canInsertFromMimeData(self, source):
        if source.hasImage() or source.hasUrls() or source.hasHtml():
            return True
        return super().canInsertFromMimeData(source)

    def insertFromMimeData(self, source):
        """Handle paste and drop events with image support."""
        # Try each method in order of reliability.

        # 1. Check for direct image data.
        if source.hasImage():
            image = source.imageData()
            if isinstance(image, QImage) and not image.isNull():
                self.insert_image(image)
                return

        # 2. Check for URLs (local or remote).
        if source.hasUrls():
            for url in source.urls():
                local_path = url.toLocalFile()
                if local_path and self.is_image_file(local_path):
                    self.insert_image_from_path(local_path)
                    return

                # Try downloading remote URL.
                url_string = url.toString()
                if url_string and self.looks_like_image_url(url_string):
                    local_path = download_image(url_string)
                    if local_path:
                        self.insert_image_from_path(local_path)
                        return

        # 3. Check for HTML with embedded image tags.
        if source.hasHtml():
            image_url = extract_image_url_from_html(source.html())
            if image_url:
                if image_url.startswith("data:"):
                    # Data URI &amp;mdash; decode and insert.
                    image = self.image_from_data_uri(image_url)
                    if image and not image.isNull():
                        self.insert_image(image)
                        return
                else:
                    local_path = download_image(image_url)
                    if local_path:
                        self.insert_image_from_path(local_path)
                        return

        # Fall back to default behavior for plain text, etc.
        super().insertFromMimeData(source)

    def insert_image_from_path(self, file_path):
        """Insert an image from a local file path into the editor."""
        image = QImage(file_path)
        if image.isNull():
            print(f"Could not load image: {file_path}")
            return
        self.insert_image(image)

    def insert_image(self, image):
        """Insert a QImage into the editor at the current cursor position."""
        cursor = self.textCursor()
        document = self.document()

        # Add the image as a resource in the document.
        image_name = f"dropped_image_{id(image)}"
        document.addResource(
            document.ResourceType.ImageResource.value,
            self.create_url(image_name),
            image,
        )

        # Insert the image at the cursor.
        image_format = cursor.charFormat()
        from PyQt6.QtGui import QTextImageFormat

        img_fmt = QTextImageFormat()
        img_fmt.setName(image_name)
        img_fmt.setWidth(min(image.width(), 600))
        img_fmt.setHeight(
            int(image.height() * min(image.width(), 600) / max(image.width(), 1))
        )
        cursor.insertImage(img_fmt)

    @staticmethod
    def create_url(name):
        from PyQt6.QtCore import QUrl

        return QUrl(name)

    @staticmethod
    def is_image_file(path):
        extensions = {".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".svg"}
        return os.path.splitext(path.lower())[-1] in extensions

    @staticmethod
    def looks_like_image_url(url_string):
        """Check if a URL looks like it points to an image."""
        clean_url = url_string.split("?")[0].lower()
        extensions = {".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".svg"}
        return any(clean_url.endswith(ext) for ext in extensions)

    @staticmethod
    def image_from_data_uri(data_uri):
        """Decode a data: URI and return a QImage."""
        import base64

        try:
            # data:image/png;base64,iVBOR...
            header, data = data_uri.split(",", 1)
            image_data = base64.b64decode(data)
            image = QImage()
            image.loadFromData(image_data)
            return image
        except Exception as e:
            print(f"Failed to decode data URI: {e}")
            return None


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Rich Text Editor &amp;mdash; Image Drop Demo")
        self.setMinimumSize(700, 500)

        self.editor = ImageDropTextEdit()
        self.editor.setPlaceholderText(
            "Try dragging an image from your web browser into this editor..."
        )

        layout = QVBoxLayout()
        layout.addWidget(self.editor)

        container = QWidget()
        container.setLayout(layout)
        self.setCentralWidget(container)


app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 id="whats-happening-in-the-complete-example"&gt;What's happening in the complete example&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;ImageDropTextEdit&lt;/code&gt; class overrides &lt;code&gt;insertFromMimeData&lt;/code&gt;, which Qt calls for both paste (Ctrl+V) and drag-and-drop operations. This gives you a single place to handle all image insertion.&lt;/p&gt;
&lt;p&gt;The method tries each data source in order:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Direct image data&lt;/strong&gt; &amp;mdash; the fastest and most reliable, when available.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;URLs&lt;/strong&gt; &amp;mdash; first checking for local files, then attempting to download remote URLs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HTML fragments&lt;/strong&gt; &amp;mdash; parsing out &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tags and fetching the referenced image, including support for &lt;code&gt;data:&lt;/code&gt; URIs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fallback&lt;/strong&gt; &amp;mdash; if none of the above match, it passes control to the default &lt;code&gt;QTextEdit&lt;/code&gt; behavior, so normal text paste and drop still work.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;By overriding &lt;code&gt;canInsertFromMimeData&lt;/code&gt; as well, we tell Qt's drag and drop system that our editor accepts these additional formats, which ensures the correct cursor icon appears when hovering over the editor.&lt;/p&gt;
&lt;p&gt;This approach handles the differences between browsers &amp;mdash; Chrome, Firefox, Edge &amp;mdash; and between different types of images on the web, making your rich text editor's drag and drop support much more resilient.&lt;/p&gt;
            &lt;p&gt;For an in-depth guide to building Python GUIs with PyQt6 see my book, &lt;a href="https://www.martinfitzpatrick.com/pyqt6-book/"&gt;Create GUI Applications with Python &amp; Qt6.&lt;/a&gt;&lt;/p&gt;
            </content><category term="pyqt6"/><category term="pyqt"/><category term="drag-and-drop"/><category term="rich-text"/><category term="python"/><category term="mime-data"/><category term="qt"/><category term="qt6"/></entry></feed>