Quill JS 使用 base64 内联图片时如何保留原始文件名?
读取文件时,可以从 File.name 取得文件名,将它写入图片的 data-filename 属性,再随 HTML 正文发送给 Python 后端。
需要改动两处:接管工具栏的图片按钮,取得用户选择的 File;扩展 Quill 的图片格式,让编辑器识别并保留文件名。仅在 DOM 上调用 setAttribute(),无法保证文件名在保存 Delta、重新加载内容后仍然保留。
为什么 base64 中没有文件名?
FileReader.readAsDataURL(file) 生成的内容通常如下:
data:image/png;base64,iVBORw0KGgo...
其中只有媒体类型和文件数据,没有原始文件名。因此,选择文件时需要单独保存 file.name。后端如果只收到 Data URL,就无法可靠地还原原始文件名。
1. 扩展图片格式,保存 data-filename
在创建 Quill 实例之前注册以下格式。图片仍使用原有的字符串值,文件名作为附加格式保存,image 的值结构保持不变。
import Quill from 'quill';
const BaseImage = Quill.import('formats/image');
const Delta = Quill.import('delta');
class ImageWithFilename extends BaseImage {
static formats(domNode) {
const formats = { ...super.formats(domNode) };
const filename = domNode.getAttribute('data-filename');
if (filename !== null) {
formats.filename = filename;
}
return formats;
}
format(name, value) {
if (name === 'filename') {
if (typeof value === 'string' && value.length > 0) {
this.domNode.setAttribute('data-filename', value);
} else {
this.domNode.removeAttribute('data-filename');
}
return;
}
super.format(name, value);
}
}
Quill.register('formats/image', ImageWithFilename, true);
filename 是 Quill 内部的格式名,对应 HTML 中的 data-filename 属性。formats() 负责读取,format() 负责写入和删除。
注册代码应放在模块作用域,使用与创建编辑器时相同的 Quill 导入。如果项目已有自定义图片格式,需要将文件名逻辑合并进去,避免覆盖已有功能。
2. 接管工具栏的图片按钮
自定义处理器会创建文件选择框,并在读取图片时一并保存 file.name:
function handleImage() {
const quill = this.quill;
const range = quill.getSelection(true);
const savedIndex = range
? range.index
: quill.getLength() - 1;
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.onchange = () => {
const file = input.files?.[0];
if (!file) return;
// 这里只是前端初步检查,后端仍需验证文件内容。
if (!file.type.startsWith('image/')) {
console.error('请选择图片文件');
return;
}
const reader = new FileReader();
reader.onload = () => {
if (typeof reader.result !== 'string') return;
// 读取期间正文可能发生变化,避免索引超出范围。
const index = Math.min(
savedIndex,
quill.getLength() - 1
);
quill.updateContents(
new Delta()
.retain(index)
.insert(
{ image: reader.result },
{ filename: file.name }
),
'user'
);
quill.setSelection(index + 1, 0, 'silent');
};
reader.onerror = () => {
console.error('图片读取失败', reader.error);
};
reader.readAsDataURL(file);
};
input.click();
}
处理器需要使用普通函数,因为 Quill 会通过 this.quill 提供编辑器实例。
将原配置中的 modules.toolbar 数组替换为下面的对象,保留其余模块配置:
toolbar: {
container: [
[{ header: [1, 2, false] }],
['bold', 'italic', 'underline', 'strike', 'blockquote'],
[{ list: 'ordered' }, { list: 'bullet' }],
[{ align: [] }],
['link', 'image'],
['clean'],
['table-better'],
],
handlers: {
image: handleImage,
},
},
插入后的图片 HTML 类似:
<img
src="data:image/png;base64,..."
data-filename="产品截图.png"
>
对应的 Delta 会包含文件名:
{
insert: {
image: 'data:image/png;base64,...'
},
attributes: {
filename: '产品截图.png'
}
}
这是单个 Delta operation。图片和文件名通过同一次 updateContents() 写入,后续可以一起保存。
3. 将 HTML 发送给后端
提交时读取编辑器内容:
const html = quillInstance.current.root.innerHTML;
// 将 html 放入现有的请求正文中。
const payload = { html };
检查实际发送的请求数据,确认其中包含 data-filename。如果发送前还有 HTML 清洗或转换步骤,也需要保留这个属性。
Python 后端可以用 HTML 解析器提取文件名和图片数据。下面使用 Beautiful Soup,运行环境需要先安装 beautifulsoup4:
import base64
from bs4 import BeautifulSoup
def extract_inline_images(html):
soup = BeautifulSoup(html, "html.parser")
images = []
allowed_types = {
"image/png",
"image/jpeg",
"image/gif",
"image/webp",
}
for img in soup.find_all("img"):
src = img.get("src", "")
header, separator, encoded = src.partition(",")
if not separator or not header.startswith("data:"):
continue
if not header.endswith(";base64"):
continue
media_type = header[5:-7].lower()
if media_type not in allowed_types:
continue
filename = img.get("data-filename") or "image"
# 不把客户端文件名当成文件路径使用。
filename = filename.replace("\\", "/").rsplit("/", 1)[-1]
filename = "".join(
char for char in filename if ord(char) >= 32
).strip() or "image"
# 无效的 base64 会抛出异常,由请求处理层返回错误。
content = base64.b64decode(encoded, validate=True)
images.append({
"filename": filename,
"content_type": media_type,
"content": content,
})
return images
提取出的 filename 可以传给邮件库的附件命名参数。例如,使用 Python 的 EmailMessage.add_attachment() 添加普通附件:
for image in extract_inline_images(html):
maintype, subtype = image["content_type"].split("/", 1)
message.add_attachment(
image["content"],
maintype=maintype,
subtype=subtype,
filename=image["filename"],
)
这里假设 message 已创建并设置好正文。如果图片需要显示在邮件正文中,应将它添加为关联的 MIME 图片部分,设置 Content-ID,并将 HTML 中对应的 src 改为 cid:...。data-filename 只传递文件名,不能替代邮件的 MIME 结构。
也可以把图片信息单独提交
如果不想在 HTML 中保存附件元数据,可以给图片分配唯一 ID:
<img data-image-id="image-001" src="data:image/png;base64,...">
再随请求提交:
{
"html": "<p>图片:</p><img data-image-id=\"image-001\" src=\"data:image/png;base64,...\">",
"images": [
{
"id": "image-001",
"filename": "产品截图.png"
}
]
}
采用这种方式,也需要让 Quill 保留 data-image-id,或在提交时建立对应关系。关联图片应使用唯一 ID,因为同一份图片数据可能来自不同文件名,文件名本身也可能重复。
使用时需要注意什么?
- 上面的处理器只接管工具栏图片按钮。粘贴、拖拽或第三方上传模块需要分别处理。只有拿到
File,才能读取其name,而剪贴板中的图片未必带有原始文件名。 - 如果配置了 Quill 的
formats白名单,需要同时允许image和新增的filename。 - 图片读取是异步操作。示例保存了插入位置,并限制索引范围。如果需要在读取期间持续编辑后仍准确跟随原位置,就要根据期间的 Delta 变化调整索引。
- 后端应限制请求体和解码后的图片大小,并验证实际文件内容。
accept、file.type和 Data URL 中的媒体类型都不能代替后端校验。 - Quill 和图片缩放插件的具体版本尚未提供,插件兼容性仍需确认。接入后应检查缩放、撤销和重做功能,并确认保存、重新加载内容后,
data-filename是否仍然保留。
备注:内容仅供参考。
