Skip to content

Instantly share code, notes, and snippets.

@w568w
Last active June 13, 2025 03:57
Show Gist options
  • Select an option

  • Save w568w/3b180b19cff4325fcf457bc77cd5fa8b to your computer and use it in GitHub Desktop.

Select an option

Save w568w/3b180b19cff4325fcf457bc77cd5fa8b to your computer and use it in GitHub Desktop.
Add MIME types when copying images from ill-behaviored clients

Quirks of QQ during copy-pasting

  1. Regardless of whether Wayland or X11 is used, it exclusively calls the X11 clipboard interface, and often fails to do so properly (e.g., sometimes requiring 3-4 repeated Ctrl+C presses to copy content to the system clipboard. However, after the first Ctrl+C press, content can be freely copied/pasted within QQ itself while completely ignoring the system clipboard's existing content.)
  2. When copying static images, it only adds a text/html entry (Update: This was a misunderstanding - CopyQ failed to detect image/* data from QQ for unknown reasons, only showing text/html. The actual issue stems from Feishu's side, which only supports text/uri-list format. Maybe turn to alternative clipboard manager?). This results in only highly friendly applications like Telegram being able to paste images copied from QQ, while others (e.g., Feishu) completely fail to paste QQ-sourced images.
  3. Sometimes image copies produce invalid file paths (i.e., links pointing to empty files). Simultaneously, attempting to view the original image within QQ will show "resource loading failed" errors.

This snippet tries to fix Problem 2.

How to use?

  1. Install htmlq (for HTML parsing) and copyq (a clipboard manager supporting custom scripts);
  2. Check https://github.com/hluk/copyq-commands/ for how to install the script into Copyq.
[Command]
Automatic=true
Command="
copyq:
function hasHtmlDataAndNoImageData(formats) {
let hasHtml = false;
let hasImage = false;
for (const format of formats) {
if (format === mimeHtml) {
hasHtml = true;
} else if (format.startsWith('image/')) {
hasImage = true;
}
}
return hasHtml && !hasImage;
}
function isLegalImageHtml(htmlStr) {
const result = execute('htmlq', 'body > *', null, htmlStr);
if (!result || result.exit_code !== 0) {
return false;
}
const html = str(result.stdout).trim();
if (html.split('\\n').length > 1) {
return false;
}
const image = execute('htmlq', 'img', '--attribute', 'src', null, html);
if (!image || image.exit_code !== 0) {
return false;
}
const src = str(image.stdout).trim();
if (!src.startsWith('file://')) {
return false;
}
const filePath = src.substring(7);
if (!filePath) {
return false;
}
return filePath;
}
function guessMimeType(filePath) {
filePath = filePath.toLowerCase();
if (filePath.endsWith('.jpg') || filePath.endsWith('.jpeg')) {
return 'image/jpeg';
} else if (filePath.endsWith('.png')) {
return 'image/png';
} else if (filePath.endsWith('.gif')) {
return 'image/gif';
} else if (filePath.endsWith('.bmp')) {
return 'image/bmp';
} else if (filePath.endsWith('.webp')) {
return 'image/webp';
}
return 'image/jpeg';
}
const formats = dataFormats();
if (!hasHtmlDataAndNoImageData(formats)) abort();
const htmlData = clipboard(mimeHtml);
const filePath = isLegalImageHtml(htmlData);
if (filePath === false) abort();
let f = new File(filePath);
if (!f.openReadOnly()) abort();
const imageData = f.readAll();
f.close();
let newClipboard = {
'text/html': htmlData,
'text/uri-list': 'file://' + filePath
};
newClipboard[guessMimeType(filePath)] = imageData;
copy(newClipboard);
"
Icon=\xe4e2
Name=Append image data and URI list to text/html only clipboard
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment