18

一、背景概述

前阵子听到公司运营的小姐姐们在抱怨,说在富文本编辑器中发布包含图片的 Word 文档时,图片和文本内容不能一起复制,每次她们都得分开处理,对于包含较多图片的 Word 时,她们处理起来很抓狂。目前她们所使用后台的富文本编辑器是 Ueditor,刚好近期也在研究一款富文本编辑器 —— Editor.jsblock styled editor ),也会遇到这种问题,所以就自觉揽下这个小任务。

要解决上述的问题,首先就需要能够解析 Word 文档中的图片。目前 Word 有两种格式后缀分别是 .doc 和 .docx。97-2003 的旧版本文件名后缀就是 .doc, 2007 版以后的后缀名是 .docx。docx 格式是被压缩过的文档,体积更小,能处理更加复杂的内容,访问速度更快。

对于上述两种格式的 Word 文档,大家应该都很熟悉。但估计挺多小伙伴不知道 Word 文档是如何存储内容的,这里我们以 docx 格式为例。这里我已经提前准备了一个包含图片和文本的 word2html.docx 文件,然后复制一份重命名为 word2html.zip。看到 zip 后缀相信你已经猜到了,下一步我们要执行解压操作。当完成解压操作之后,默认在当前目录会生成一个 word2html 文件夹,该文件夹的主要目录结构如下:

word-unzip-structure.jpg

感兴趣的小伙伴可以自行解压一下 Word 文档,简单分析一下解压后的文件。

经过本人认真观察后发现,在解压后 Word 文档中包含的图片会被保存到 word/media 目录下。而我们要解决的问题就是能识别到 Word 文档中的图片,然后自动上传到文件资源服务器。要实现这个功能的前提就是能够解析当前的 Word 文档,值得庆幸的是这个功能已经有前人帮我们实现了。

对于 Java 开发者来说,可以直接基于 POI 项目,POI 是 Apache 的一个开源项目,它的初衷是处理基于 Office Open XML 标准(OOXML)和 Microsoft OLE 2 复合文档格式(OLE2)的各种文件格式的文档,而且支持读写操作。当然本文的重点不是服务端解析方案,而是在前端如何实现 Word 解析并提取 Word 中的图片。同样对于纯前端的解析方案,mwilliamson 大佬已经帮我们实现了,下面我们来简单介绍一下 Mammoth.js 这个库。

二、Mammoth.js

2.1 Mammoth.js 简介

Mammoth 旨在转换 .docx 文档(例如由 Microsoft Word 创建的文档),并将其转换为 HTML。 Mammoth 的目标是通过使用文档中的语义信息并忽略其他细节来生成简单干净的 HTML。比如,Mammoth 会将应用标题 1 样式的任何段落转换为 h1 元素,而不是尝试完全复制标题的样式(字体,文本大小,颜色等)。

由于 .docx 使用的结构与 HTML 的结构之间存在很大的不匹配,这意味着对于较复杂的文档而言,这种转换不太可能是完美的。但如果你仅使用样式在语义上标记文档,则 Mammoth 能实现较好的转换效果。当前 Mammoth 支持以下主要特性:

  • Headings
  • Lists,Table
  • Images
  • Bold, italics, underlines, strikethrough, superscript and subscript
  • Links,Line breaks
  • Footnotes and endnotes

它还支持自定义映射规则。例如,你可以通过提供适当的样式映射将 WarningHeading 转换为 h1.warning。另外文本框的内容被视为单独的段落,出现在包含文本框的段落之后。

2.2 Mammoth.js API

Mammoth.js API 为我们提供了很多方法,这里我们来介绍三个比较常用的 API:

  • mammoth.convertToHtml(input, options:把源文档转换为 HTML 文档
  • mammoth.convertToMarkdown(input, options):把源文档转换为 Markdown 文档。这个方法与 convertToHtml 方法类似,区别就是返回的 result 对象的 value 属性是 Markdown 而不是 HTML。
  • mammoth.extractRawText(input):提取文档的原始文本。这将忽略文档中的所有格式。每个段落后跟两个换行符。

介绍完 Mammoth.js 相关的特性和 API,接下来我们开始进入实战环节。

三、Mammoth.js 实战

Mammoth.js 这个库同时支持 Node.js 和浏览器两个平台,在浏览器端 mammoth.convertToHtml 方法的 input 参数的格式是 {arrayBuffer: arrayBuffer},其中 arrayBuffer 就是 .docx 文件的内容。在前端我们可以通过 FileReader API 来读取文件的内容,此外该接口也提供了 readAsArrayBuffer 方法,用于读取指定的 Blob 中的内容,一旦读取完成,result 属性中保存的将是被读取文件的 ArrayBuffer 数据对象。下面我们定义一个 readFileInputEventAsArrayBuffer 方法:

export function readFileInputEventAsArrayBuffer(event, callback) {
  const file = event.target.files[0];

  const reader = new FileReader();

  reader.onload = function(loadEvent: Event) {
    const arrayBuffer = loadEvent.target["result"];
    callback(arrayBuffer);
  };

  reader.readAsArrayBuffer(file);
}

该方法用于实现把输入的 file 对象转换为 ArrayBuffer 对象。在获取 Word 文档的 ArrayBuffer 对象之后,就可以调用 convertToHtml 方法,把 Word 文档内容转换为 HTML 文档。

mammoth.convertToHtml({ arrayBuffer })

如果你的文档中不包括特殊的图片类型,比如 wmfemf 类型,而是常见的 jpgpng 等类型的话,那么你可以看到 Word 文档中的图片。难道这样就搞定了,那是不是太简单了,其实这只是刚开始。当你通过浏览器的开发者工具审查 Word 解析后的 HTML 文档后,会发现图片都以 Base64 的格式进行嵌入。如果图片不多且单张图片也不会太大的话,那这种方案是可以考虑的。针对这种情况,一种比较好的方案是把图片提交到文件资源服务器上。

在 Mammoth.js 中要实现上述的功能,可以使用 convertImage 配置选项来自定义图片处理器。使用示例如下:

var options = {
    convertImage: mammoth.images.imgElement(function(image) {
        return image.read("base64").then(function(imageBuffer) {
            return {
                src: "data:" + image.contentType + ";base64," + imageBuffer
            };
        });
    })
};

上面示例实现的功能就是把图片转成 Base64 的格式,很明显不符合我们的要求。这里我们需要做以下调整:

const mammothOptions = {
  convertImage: mammoth.images.imgElement(function(image) {
    return image.read("base64").then(async (imageBuffer) => {
      const result = await uploadBase64Image(imageBuffer, image.contentType);
      return {
        src: result.data.path // 获取图片线上的URL地址
      };
    });
  })
};

顾名思义 uploadBase64Image 方法的作用就是上传 Base64 格式的图片:

async function uploadBase64Image(base64Image, mime) {
  const formData = new FormData();
  formData.append("file", base64ToBlob(base64Image, mime));
  return await axios({
    method: "post",
    url: "http://localhost:3000/uploadfile", // 本地图片上传的API地址
    data: formData,
    config: { headers: { "Content-Type": "multipart/form-data" } }
  });
}

为了减少图片文件的大小,我们需要把 Base64 格式的图片先转成 Blob 对象,然后在通过创建 FormData 对象进行提交。base64ToBlob 方法的定义如下:

export function base64ToBlob(base64, mime) {
  mime = mime || "";
  const sliceSize = 1024;
  const byteChars = window.atob(base64);
  const byteArrays = [];
  for (
    let offset = 0, len = byteChars.length;
    offset < len;
    offset += sliceSize
  ) {
    const slice = byteChars.slice(offset, offset + sliceSize);

    const byteNumbers = new Array(slice.length);
    for (let i = 0; i < slice.length; i++) {
      byteNumbers[i] = slice.charCodeAt(i);
    }

    const byteArray = new Uint8Array(byteNumbers);

    byteArrays.push(byteArray);
  }

  return new Blob(byteArrays, { type: mime });
}

至此解析 Word 文档并自动把文档中的图片上传至文件资源服务器的基本功能已经实现了。目前该方案遇到的问题就是无法处理 wmfemf 类型的图片文件,针对这个问题一开始就想到了七牛云的图片处理服务,但阅读官方相关的使用文档后,发现所有的图片处理服务均不支持 wmfemf 类型。当然,期间也尝试了国外在线的图片格式化服务和网上一些大佬提供的格式化方案,可惜的是最终的效果都不好,所以对于这种特殊的图片格式目前的解决方案就是让用户手动上传对应原始图片,如果小伙伴们有好的方案,欢迎给我留言哟。

四、参考资源


阿宝哥
15.8k 声望10.2k 粉丝

聚焦全栈,专注分享 Angular、TypeScript、Node.js/Java 、Spring 技术栈等全栈干货