浏览器复制 GIF 为什么总会丢动画:剪贴板格式和插件方案
最近又遇到一个很古早的问题:在新版 Edge 里右键复制一张 GIF,粘到聊天工具里,动画还是经常变成第一帧。
这个问题容易被误判成「浏览器不支持 GIF」或者「聊天工具不支持 GIF」。实际更绕一点:浏览器当然能播放 GIF,聊天工具也常常能发送 GIF;真正卡住的是中间那层系统剪贴板。右键「复制图片」时,浏览器往剪贴板里写的未必是原始 GIF 文件,目标应用也未必会按网页里的 img src 去重新下载原图。
同一张 GIF 有几种复制路径
同一句「复制 GIF」,背后可能是几条完全不同的路径:

右键「复制图片」最容易丢动画。浏览器通常把当前图片写成位图或 PNG 一类的静态图片格式,GIF 的多帧信息在这一步就没了。
选中网页区域再复制,有时反而能动。这时起作用的是剪贴板里的 text/html,里面保留了 <img src="...">。目标应用如果愿意解析 HTML,就能拿到图片 URL,再自己下载一遍;浏览器并没有把 GIF 文件本身写进去。
下载 GIF 文件、从资源管理器复制文件、或者直接把文件拖到聊天窗口,通常最稳。此时目标应用拿到的是文件路径或文件对象,动画仍然在原始文件里。
剪贴板不是一个图片槽
系统剪贴板不是一个只能放图片的槽。一次复制可以同时放很多格式,粘贴方会挑自己能读、也愿意读的那一种。

在 Windows 里,普通位图、DIB、文件列表是不同格式。微软的 Shell Clipboard 文档里把 CF_HDROP 定义为传输一组既有文件位置的格式,里面放的是一个双 null 结尾的完整文件路径数组。这个格式很适合表达「我复制了一个文件」,但它不是普通网页 Clipboard API 随手能写进去的东西。
macOS 也类似。Pasteboard 里可以有 public.tiff、public.png、public.file-url、public.gif 等不同类型。目标应用是否保留动画,取决于它读到的是静态图片数据、GIF 数据,还是一个可以读取的文件 URL。
这也解释了一个老现象:早年 IE 能把网页缓存里的 GIF 文件路径塞到剪贴板里,QQ 之类的应用拿到文件路径后就能发送会动的图;Chrome / Firefox 只写静态图片时,粘贴结果就剩第一帧。
新版 Edge 为什么仍然没修好
排查时,Chromium 的 Async Clipboard: GIF support 仍是开放的 feature request。下面的支持探针与仓库问题都属于这次记录;浏览器或上游更新后,应重新测试,而不是把当时状态当成永久限制。问题也不只是「把 image/gif 放进白名单」,还要考虑:
- 渲染进程和浏览器进程之间如何安全传输 GIF 容器。
- 写入系统剪贴板前如何做解码、编码或安全清洗。
- Windows、macOS、Linux 各自应该写什么系统格式。
- Blink 里 selection copy、image copy、Clipboard API 写入是不是走同一套行为。
- 目标应用是否真的识别新增格式。
在当前 Edge 环境里,用 ClipboardItem.supports() 探了一下,结果也符合这个判断:

ClipboardItem.supports('text/plain') // true
ClipboardItem.supports('text/html') // true
ClipboardItem.supports('image/png') // true
ClipboardItem.supports('image/svg+xml') // true
ClipboardItem.supports('image/gif') // false
ClipboardItem.supports('web image/gif') // trueMDN 对 ClipboardItem.supports() 的说明也能对上:text/plain、text/html、image/png 是 Clipboard API 要求支持的类型;image/svg+xml 和以 web 开头的自定义 MIME 类型属于可能支持的扩展项。
这里最容易误会的是 web image/gif。Chrome 的 Web custom formats for the Async Clipboard API 区分了 Web 自定义格式与原生格式:web image/jpeg、web image/gif 这类格式不是普通桌面应用天然认识的格式。它们适合网页应用之间约定读写,平台原生应用通常仍然期待 image/jpeg、文件路径或系统原生格式。
因此,纯 Web Clipboard API 目前不能把一张 GIF 稳定写成「QQ、微信、Slack、图片编辑器都能当作动图文件读取」的系统剪贴板内容。
纯扩展只能缓解,不能真正补齐系统格式
浏览器扩展能做的事不少。它可以加右键菜单,可以拿到被右键的图片 URL,可以写 text/html,也可以在不支持 GIF 时退成首帧 PNG。对网页编辑器、邮件编辑器、部分在线聊天应用,这已经有价值。
但纯扩展有一道硬边界:它还在浏览器沙箱里。它不能随便把本地文件列表写成 Windows CF_HDROP,也不能像桌面程序一样操作 macOS Pasteboard 的文件 URL 和原生类型。

Native Messaging 可以把这部分工作交给本地程序。Microsoft Edge 的 Native Messaging 文档介绍了这种通道:扩展通过标准输入输出和用户设备上已经安装的 native app host 通信,Edge 不负责安装或管理那个 native app。Chrome 的同类文档也说明了 host manifest、allowed_origins、stdio 协议和消息大小限制。
扩展能拿到网页上下文,本机 host 才能碰系统能力。关于这套「插件发结构化消息,host 分发本地能力」的机制,我单独整理在《让浏览器插件调用本地能力:Native Host 的边界和协议》里。GIF 复制正好卡在这条边界上。
现成项目可以参考,但不够稳
我看了两个现成项目:picasocro1/copy-gif 和 syanth/gifcopier。它们都证明了一个方向:浏览器扩展加本地程序确实能绕开纯 Clipboard API 的限制。但如果要做一个自己长期使用、还能继续加功能的工具,它们都不够稳。
copy-gif 的方向比较接近想要的样子。它有右键菜单、Native Host、Rust 代码、Windows/macOS/Linux 分支,也知道没有 Native Host 时只能退成 PNG。问题在工程一致性:仓库里 Native Host 已经是 Rust,host manifest 和 Windows 安装脚本仍然在找 copy-gif-host.py;Windows 分支把 GIF 下载到临时文件后写入文件剪贴板,但函数退出时 NamedTempFile 会清理文件,目标应用稍后读取这个路径时可能已经找不到文件;PowerShell 里构造 FileDropList 的方式也不够可靠。
这种不一致说明它更像一个还在收敛中的原型,适合借鉴思路,不适合直接拿来当正式底座。
gifcopier 更老一些,最后一次代码更新停在 2022 年。它的思路是把 GIF 或 GIFV 下载成 clip.mp4,然后把文件路径写进剪贴板。Windows 代码里可以看到这种形状:
std::string command = "curl -o clip.gif \"" + URL + "\"";
system(command.c_str());这类实现问题更多。URL 被拼进 shell 命令,风险很高;clip.gif、clip.mp4 是固定文件名,多次复制会互相覆盖;依赖用户机器上的 curl 和 ffmpeg;Windows 安装脚本要求管理员权限,还会写 Chrome 扩展白名单策略。它能说明「文件路径剪贴板」是可行的,但不适合继续扩。
这两个项目留下的有用判断是:
- 用 Native Host 是对的。
- Windows 最稳的入口是文件列表,而不是普通图片位图。
- macOS 可以走 file URL 或 GIF pasteboard 类型。
- 原始 GIF 文件必须保留到目标应用读取完成,不能写完剪贴板就删。
- 安装、注册、extension ID、浏览器差异,比右键菜单本身更容易踩坑。
自己做时的最小架构
我会把它放进已有的 Chrome Extension monorepo。仓库已有扩展应用、Vite、Vitest、zip 分发和 Native Host,可以复用安装与协议层。最初参考了 Rust 路线,后续项目把 Node.js 定为主实现,细节见前面链接的 Native Host 文章;这里讨论的是 GIF 能力方案,不要求 host 必须用 Rust,也不把方案当成已经交付的功能。
第一版先把仓库边界切成三块:

仓库结构上可以新增一个应用,再扩共享 host:
apps/copy-gif-extension/
src/background/
src/content/
src/popup/
vite.config.ts
package.json
native/m-chrome-native-host/
src/clipboard/
src/download/
src/cache/
src/install/
packages/extension-native-client/
src/client.ts
src/types.ts扩展侧不直接碰系统剪贴板。它负责:
- 在右键菜单里拿到
srcUrl、linkUrl、页面坐标和 frame 信息。 - 从
<img>、<picture>、CSS background、直接图片 URL 里解析候选图片。 - 判断 URL、MIME、文件名和站点特征,尽量找到原始 GIF,而不是预览图或转码图。
- 先调用
native.hello,确认 Native Host 版本和能力。 - 调用
clipboard.copyGifFile或类似方法,把 URL、页面来源、用户动作和可选元信息发给 host。 - Native Host 不可用时,退回
text/html、text/plain、web image/gif或首帧 PNG。
Native Host 负责:
- 下载公开 GIF,或者接收扩展传过来的二进制分片。
- 校验 magic bytes、MIME、大小、扩展名和重定向结果。
- 把文件保存到持久缓存目录,例如
%LOCALAPPDATA%或~/Library/Application Support下的版本化目录。 - 写系统剪贴板。Windows 写
CF_HDROP;macOS 写 file URL,必要时补public.gif。 - 清理过期缓存,而不是复制成功后立即删除。
- 记录 doctor 信息:host 版本、manifest 路径、allowed origins、缓存目录、最近一次错误。
文件生命周期比想象中重要
写文件路径到剪贴板时,剪贴板里只有路径,不一定有文件内容。目标应用什么时候读取文件,不由源应用控制。它可能在你按下粘贴键时读,也可能先看格式列表,等用户确认发送时才读。
因此,临时文件不能跟随一次 native 调用立刻删除。更稳的做法是把 GIF 放进受控缓存目录:
copy-gif-cache/
2026-09-27/
8d3f...a2.gif
91c0...fe.gif文件名可以用内容 hash 避免覆盖;URL hash 只能标识地址,不能证明地址里的内容没有更新。清理策略可按时间和总大小制定,比如 7 天或 512 MB,但这是容量策略,不是目标应用必定及时读取的保证。应避免立即清理当前复制项,超期文件被删除后的再次粘贴也可能失败。缓存保留期间,即使源进程退出,路径仍可供目标应用读取。
右键菜单很容易写,文件何时清理却直接影响后续粘贴。自己实现时,需要把这部分生命周期一并管好。
安全边界要比功能先定下来
Native Host 是浏览器沙箱外的本地程序,不能把它做成「扩展让我跑什么,我就跑什么」。这次只需要受控能力,不需要 shell。
请求形状可以固定成方法调用:
{
"id": "copy-1",
"method": "clipboard.copyGifFile",
"params": {
"sourceUrl": "https://example.com/a.gif",
"pageUrl": "https://example.com/post/1",
"preferredName": "a.gif"
}
}host 的能力表里只出现可审核的方法,例如:
native.hellonative.doctorclipboard.copyGifFileclipboard.copyFilecache.cleanup
不要出现 command.request 这种默认能执行任意命令的接口。即使以后要扩成本机工具平台,也应该像现有 Native Host 设计记录里写的那样,走「结构化命令 + 风险审核 + 用户授权」路线,而不是把 shell 字符串从扩展透传到本机。
下载也要收紧。最初版本可以只支持用户右键触发的当前 URL,不做后台批量抓取;不要读取 cookie,不绕登录态,不保存网页敏感内容。需要处理登录态图片时,优先让扩展在浏览器权限内 fetch,再把已拿到的字节分片交给 host,host 只负责校验和写剪贴板。
「用户右键的 URL」仍是外部输入。Host 自己下载时应限制协议和大小,逐次检查重定向,并拒绝未授权的本机、内网与文件地址,不能只检查最初的 URL。clipboard.copyFile 也应限制在 host 管理的缓存内,不接受任意本地路径;一个扩展请求中的 userAction 字段,不能独自证明已经得到授权。
跨平台实现口径
Windows 上优先直接用 Win32 API 写 CF_HDROP。不要在 Rust host 里拼 PowerShell,也不要依赖外部脚本。windows crate 或 clipboard-win 这类路线都可以评估,最终要确保:
- 文件路径是绝对路径。
- 使用 Unicode 路径。
- 写入双 null 结尾的文件列表。
SetClipboardData后内存所有权交给系统。- 目标文件在缓存目录里持续存在。
macOS 上优先写文件 URL。可以用 Objective-C / AppKit 桥接,也可以把很小的 pasteboard helper 拆成 Swift / Objective-C,再由 Rust host 调用内部 helper。只写普通文本路径不够,目标应用需要看到的是 file URL 或 GIF 数据类型。
Linux 先不急着作为第一版目标。X11、Wayland、Flatpak / Snap 沙箱、xclip / wl-copy 行为差异都比较多。等 Windows 和 macOS 稳了,再把 Linux 做成单独平台分支,避免第一版被桌面环境差异拖住。
功能完成后怎样分发
GIF 能力需要完成目标应用粘贴验证后再发布安装包。当前方案不能靠一个尚未实现该能力的 host 代替;下载包的 README 应明确它支持哪些复制路径,以及哪些目标应用经过测试。
等功能跑通后,文章资源里应该放这几类包:
source/files/2026-09-27 - 浏览器复制 GIF 为什么总会丢动画:剪贴板格式和插件方案/files/
m-chrome-native-host-0.2.0-x86_64-pc-windows-msvc.zip
m-chrome-native-host-0.2.0-aarch64-apple-darwin.zip
copy-gif-extension-0.1.0.zipWindows zip 里放:
bin/m-chrome-native-host.exe
install.ps1
README.mdmacOS zip 里放:
bin/m-chrome-native-host
install.sh
install.command
README.md安装脚本负责把 host 放到用户级应用数据目录,注册 Chrome / Edge 的 Native Messaging manifest,并把当前扩展 ID 写进 allowed_origins。插件启动时再通过 native.doctor 给用户一个明确状态:host 没装、extension ID 不匹配、浏览器 manifest 路径不对、host 版本过低,分别给不同提示。
这部分和功能代码同样重要。很多 Native Messaging 工具卡在安装细节上:用户装不上、装错浏览器、ID 不匹配,或者更新以后旧 host 仍被调用。
先让目标应用看到文件
浏览器复制 GIF 只剩第一帧,不是一个单点 bug。它横跨网页、浏览器、系统剪贴板和目标应用。只要浏览器右键复制图片时仍然把 GIF 折成普通位图,而 Web Clipboard API 又不能写普通 image/gif,这个问题就会继续存在。
日常临时解决,下载文件再拖拽或从文件管理器复制,仍然最稳。想把它变成顺手的右键菜单,就要接受「浏览器扩展 + Native Host」这条路线。
现成项目给出了可行路径,也暴露了安装和缓存生命周期上的问题。我会先复用现有 monorepo 的协议与安装框架,只实现 GIF 文件复制,并在常用目标应用中验证动画是否保留。WebP 回源、视频转换和收藏库可以等这条路径稳定以后再考虑。
目标应用需要看到它愿意按文件处理的剪贴板内容,并且在它真正读取之前,路径指向的文件必须一直存在。