CKEditor 4 从 Word 粘贴时页眉页脚图片的过滤:imagesinheaderfooter 手工测试解析与 RTF 图片提取原理

原创2026-10-15 20:58:161,634 阅读
文章标签:前端UI组件

CKEditor 4 从 Word 粘贴时页眉页脚图片的过滤:imagesinheaderfooter 手工测试解析与 RTF 图片提取原理

本篇技术指南聚焦 CKEditor 4 的 Paste from Word(粘贴自 Word)功能中一个易被忽视却十分关键的行为:当 Word 文档的页眉(header)或页脚(footer)中包含图片时,这些图片不应被粘贴进编辑器,只有正文(main content)中的图片才应被正确提取并以 Base64 内嵌形式插入。文章以仓库中 imagesinheaderfooter.md 手工测试用例为主线,结合 image.js 的 RTF 解析实现与 imagesextraction.js 自动化测试套件,说明如何复现、验证该行为,并深入剖析底层过滤原理与相关配置项。

一、测试背景:为什么页眉页脚图片需要单独处理

自 CKEditor 4.16.0 起(对应 Bender 标签 4.16.0,Ticket 2800),Paste from Word 的图片处理进行了重构:图片不再依赖 HTML 中残留的 file:// 路径,而是改为从剪贴板 RTF(Rich Text Format)数据中解析图片二进制内容,再转换为 Base64 Data URL 嵌入 <img> 标签。

这一方案的副作用在于:Microsoft Word 会把文档正文、页眉、页脚、脚注等不同节的内容连同图片一并写入 RTF 剪贴板数据。如果不过滤,粘贴后页眉/页脚中的 Logo、装饰图会意外进入编辑器正文,破坏内容语义。imagesinheaderfooter 系列测试(含 InHeader、InFooter、InHeaderFooter、InHeaderFooterComplex、InHeaderFooterCurlyBraces)正是为回归验证此行为而设计。

二、手工测试步骤与判定标准(原文档完整继承)

原测试文档 imagesinheaderfooter.md 定义了一个 Bender 手工测试用例,其头部声明如下:

@bender-tags: feature, 4.16.0, 2800, pastefromword
@bender-ui: collapsed
@bender-ckeditor-plugins: wysiwygarea, toolbar, undo, pastefromword, image, pagebreak, autogrow

1. 操作步骤

  1. 将 InHeaderFooterComplex.docx 的内容复制并粘贴到编辑器(该文档同时包含页眉图片、页脚图片与正文图片,属于"复杂"场景)。
  2. 观察编辑器内最终粘贴结果。

2. 预期结果(Expected)

  • 正文中的全部图片(不位于页眉/页脚中)与其余内容一同被粘贴出来,且渲染正确。

3. 非预期结果(Unexpected)

  • 正文图片没有被正确粘贴。
  • 页眉/页脚中的图片被粘贴进了编辑器。

与同一目录下的同类用例相比(如 imagesduplicated.md 关注重复图片去重、imagesgif.md 关注 GIF 动图与 Base64 转换),本用例的判定重点唯一且明确:来源正确性——只有正文图片可以"越界"进入编辑器,页眉页脚图片必须被丢弃。

4. 测试环境的浏览器限制

对应的测试页面 imagesinheaderfooter.html 中有一段环境守卫:

if ( bender.tools.env.mobile || CKEDITOR.env.ie || CKEDITOR.env.safari ) {
	bender.ignore();
}
CKEDITOR.replace( 'editor' );

即:移动端、IE、Safari 浏览器会直接跳过本测试。原因在于本特性依赖 Clipboard API 对自定义数据类型(text/rtf 等)的支持,该能力由 CKEDITOR.plugins.clipboard#isCustomDataTypesSupported 标志位体现,仅在现代浏览器(Chrome、Firefox 等)中可靠。手工验证时请使用 Chrome 或 Firefox。

三、自动化测试佐证:ImagesExtraction 测试套件

除了手工用例,同一场景还有自动化测试兜底。生成式测试套件 imagesextraction.js 一次性覆盖了 10 个图片提取场景:

测试名 场景说明
ImagesExtraction/InHeader 图片仅位于页眉
ImagesExtraction/InFooter 图片仅位于页脚
ImagesExtraction/InHeaderFooter 页眉页脚均有图片
ImagesExtraction/InHeaderFooterComplex 页眉页脚+正文图片的复杂组合(对应本手工用例)
ImagesExtraction/InHeaderFooterCurlyBraces 页眉页脚内容含花括号等特殊字符
ImagesExtraction/UnsupportedFormats 不支持的图片格式
ImagesExtraction/DuplicatedImage 同一图片多次出现
ImagesExtraction/DrawnObject Word 绘制的对象
ImagesExtraction/WrappedImages 文字环绕型图片
ImagesExtraction/AnimatedGif 动图 GIF

该套件的运行参数:

bender.editor = {
	config: CKEDITOR.tools.extend( pfwTools.imageDefaultConfig, {
		extraPlugins: 'pagebreak'
	} )
};

bender.test( createTestSuite( {
	browsers: [ 'chrome', 'firefox' ],
	wordVersions: [ 'word365' ],
	ignoreAll: CKEDITOR.env.ie || bender.tools.env.mobile,
	compareRawData: true,
	includeRTF: true,
	customFilters: [ pfwTools.filters.style ]
} ) );

值得注意的细节:

  • includeRTF: true:明确告知测试框架从剪贴板中读取 text/rtf 数据参与比对,这与手工用例中"复制 Word 文件内容"的操作相对应——现代浏览器复制 Word 内容时会同时携带 HTML 与 RTF 两种剪贴板格式。
  • compareRawData: true:以原始数据严格比对,保证图片二进制内容也参与断言。
  • wordVersions: ['word365']:测试夹具基于 Word 365 导出。
  • 每个场景在 tests/plugins/pastefromword/generated/_fixtures/ImagesExtraction 下都有一份 .docx 源文件、expected.html 预期输出,以及按浏览器区分的 chrome.rtf/chrome.html/firefox.rtf/firefox.html 剪贴板快照。例如 InHeaderFooterComplex/expected.html 中正文图片被转换为 data:image/png;base64,...,同时包含 pagebreak 分页符与正文文字,而页眉页脚图片完全缺席。

四、底层原理:RTF 中如何剥离页眉页脚图片

手工测试的"预期/非预期"判据,最终落在图片过滤器 plugins/pastetools/filter/image.js 的 extractFromRtf 实现上。

1. 过滤入口与顺序

Paste from Word 插件 plugin.js 在 init 中注册了三个默认过滤器:

defaultFilters = [
	CKEDITOR.getUrl( pastetoolsPath + 'filter/common.js' ),
	CKEDITOR.getUrl( pastetoolsPath + 'filter/image.js' ),
	CKEDITOR.getUrl( path + 'filter/default.js' )
];

粘贴识别(canHandle)通过 HTML 中的 class="?Mso"、mso- 样式、w:WordDocument、<o:\w+> 等特征或 <meta> generator 判定是否来自 Microsoft Office。命中后进入 handle,执行 CKEDITOR.cleanWord(...),随后在满足以下条件时调用图片过滤器(plugin.js 中对应代码):

if ( CKEDITOR.plugins.clipboard.isCustomDataTypesSupported && configInlineImages &&
	CKEDITOR.pasteFilters.image ) {
	pfwEvtData.dataValue = CKEDITOR.pasteFilters.image( pfwEvtData.dataValue, editor, dataTransferRtf );
}

其中 configInlineImages 即 config.pasteFromWord_inlineImages(默认 true)。可见剪贴板 RTF 支持与内嵌图片开关是图片提取生效的前提,这也解释了上一节中的浏览器限制。

2. 关键一步:先删页眉页脚,再提图片

image.js 的 extractFromRtf 函数 中有一段注释与正则,正是本测试主题的源码级答案:

// Remove headers, footers, non-Word images and drawn objects.
// Headers and footers are in \header* and \footer* groups,
// non-Word images are inside \nonshp groups.
// Drawn objects are inside \shprslt and could be e.g. image alignment.
rtfContent = filter.removeGroups( rtfContent, '(?:(?:header|footer)[lrf]?|nonshppict|shprslt)' );
wholeImages = filter.getGroups( rtfContent, 'pict' );

处理顺序为:

  1. 用正则 (?:header|footer)[lrf]? 匹配并删除 RTF 中所有 \header*/\footer* 分组(l/r/f 后缀覆盖 left/right/first 页眉页脚变体);
  2. 同时删除 \nonshppict(非 Word 图片)与 \shprslt(绘制对象结果)分组;
  3. 再通过 getGroups( rtfContent, 'pict' ) 提取剩余 \pict 图片分组。

removeGroups/getGroups 的通用实现位于 plugins/pastetools/filter/common.js,其核心是基于分组嵌套的匹配与内容截断替换。由于页眉页脚分组已在提取前被整体移除,其中的图片永远不会进入后续 \pict 扫描结果——这正是"页眉页脚图片不应被粘贴"这一预期结果的实现机制。

3. 图片类型识别与 Base64 转换

提取到的每个 \pict 分组会经过类型识别(recognizableImageTypes):

{ marker: /\\pngblip/,  type: 'image/png'  },
{ marker: /\\jpegblip/, type: 'image/jpeg' },
{ marker: /\\emfblip/,  type: 'image/emf'  },
{ marker: /\\wmetafile\d/, type: 'image/wmf' }

只有 supportedImageTypes = <a href="https://link.gitcode.com/i/34215d1ca73d4341a28f5a5cb1583f17" target="_blank">'image/png', 'image/jpeg', 'image/gif']([定义位置)会被编码为 data:<type>;base64,...(createSrcWithBase64),其余格式记为 type: 'unknown' 并跳过,对应测试套件中的 UnsupportedFormats 场景。此外,重复图片(相同 \blipuid/\bliptag 标识)、WordArt(\defshp)、水平线(fHorizRule)等也有专门分支处理(extractFromRtf 内部分支),对应 DuplicatedImage、DrawnObject、pasteimagewithhrline 等测试。

4. 计数一致性校验

handleRtfImages(image.js)会对比"RTF 中提取的图片数"与"HTML <img> 标签数",不一致时输出 pastetools-failed-image-extraction 错误并放弃替换;对不支持的图片类型输出 pastetools-unsupported-image。这就是 imagesduplicated.md 中要求"控制台不得出现 pastetools 相关错误"的原因。在本用例(页眉页脚图片)中,由于页眉页脚分组已被提前删除,RTF 与 HTML 两侧的图片计数恰好保持一致,不会触发错误日志。

五、相关配置项速查

结合 plugin.js 文档注释,与本功能相关的配置如下:

配置项 默认值 说明
config.pasteFromWord_inlineImages true 是否将 Word 内容中的图片以内嵌(Base64)方式粘贴;false 时跳过图片过滤器
config.pasteFromWordPromptCleanup false 粘贴 Word 内容前是否弹出清理确认框
config.pasteFromWordCleanupFile <plugin path> + 'filter/default.js' 自定义 Word 清理过滤器文件路径(全局配置,所有实例共享)
config.pasteFromWord_keepZeroMargins false 已废弃(4.13.0 起),迁移至 pasteTools_keepZeroMargins

关闭内嵌图片的示例:

config.pasteFromWord_inlineImages = false;

六、如何运行与验证

  1. 自动化验证:在仓库根目录使用 Bender 运行生成式测试套件:
bender run tests/plugins/pastefromword/generated/imagesextraction.js
  1. 手工验证:运行 imagesinheaderfooter.html(Bender 手工测试模式),在 Chrome/Firefox 中打开编辑器,复制 InHeaderFooterComplex.docx 内容粘贴,观察正文图片正常内嵌、页眉页脚图片不出现;同时可打开浏览器控制台确认无 pastetools-* 错误。

  2. 源码对照:若想亲手验证过滤逻辑,可在 extractFromRtf 中临时输出 rtfContent 观察 \header/\footer 分组被删除后的结果(仅限本地调试,仓库本身为只读)。

总结

imagesinheaderfooter 手工测试用例表面上看只是一条"复制粘贴并目测"的验收条目,实则验证了 Paste from Word 图片管线中一条重要的一致性约束:RTF 剪贴板中的页眉页脚图片必须在提取阶段被整体丢弃,且不得干扰正文图片的 Base64 内嵌与计数校验。理解 removeGroups( rtfContent, '(?:(?:header|footer)[lrf]?|nonshppict|shprslt)' ) 这一行代码,就等于掌握了从 Word 粘贴时"哪些图片会进来、哪些图片不会进来"的全部规则。

登录后查看全文
ckeditor4