docx 文件定制指南

(发布于 2015 年 6 月 1 日)

我在“翻译时使用的应用”一文中提到,因为最近翻译的一本书要求提供 docx 文件,所以我第一次使用了 pandoc

Pandoc 是个很强大的工具,能在多种文件格式之间相互转换。不过,我使用 Pandoc 只是为了把 Markdown 文件转换成 docx 文件,以便提供给出版社。Pandoc 能很好地完成这项工作,不过转换得到的 docx 文件格式不符合我的要求,所以最近几天稍微研究了定制样式的问题。

docx 格式简介

docx 是微软为 Word 软件开发的文件格式,其背后是一个国际标准——Office Open XML(简称 OOXML)。docx 其实是打包文件,把扩展名从 docx 改为 zip 就能使用解压软件打开,看到其中的内容。解压后的文件结构如下所示:

.
├── [Content_Types].xml
├── _rels
├── docProps
│   ├── app.xml
│   └── core.xml
└── word
    ├── _rels
    │   ├── document.xml.rels
    │   └── footnotes.xml.rels
    ├── document.xml
    ├── fontTable.xml
    ├── footnotes.xml
    ├── media
    ├── numbering.xml
    ├── settings.xml
    ├── styles.xml
    ├── theme
    │   └── theme1.xml
    └── webSettings.xml

可以看出,大多数都是 XML 文件。我们主要关注其中三个文件:word/document.xmlword/styles.xmlword/fontTable.xml

  • document.xmldocx 文件的内容
  • styles.xmldocx 文件的样式
  • fontTable.xmldocx 文件用到的的字体

docx 文件基本上做到了表现和内容分离。document.xmlstyles.xml 可以比作 Web 中的 HTML 和 CSS。定制样式时,我们基本上不用修改内容,也就是 document.xml 文件。下面着重介绍 styles.xml 文件。

styles.xml 文件的作用相当于 Web 中的 CSS,只不过这是 XML 文件,而且也不使用 CSS 规则定义样式。下面是一个样式示例:

<w:style w:styleId="BodyText" w:type="paragraph">
    <w:name w:val="Body Text"/>
    <w:basedOn w:val="Normal"/>
    <w:link w:val="BodyTextChar"/>
    <w:pPr>
        <w:spacing w:after="180" w:before="180"/>
    </w:pPr>
    <w:rPr>
        <w:rFonts w:ascii="Arial" w:cstheme="majorBidi" w:eastAsia="黑体" w:hAnsiTheme="majorHAnsi"/>
        <w:sz w:val="36"/>
    </w:rPr>
</w:style>

w:styleId 属性的值是内部 ID,document.xml 文件中的结构需要什么样式,就引用这个 ID 的值,就像是 CSS 中的选择符一样。w:type 属性是样式的类型,一般只会用到 paragraphcharactertablew:name 元素中 w:val 属性的值是这个样式在 GUI 应用(Word,Pages 或 OpenOffice 等)中显示的名称。w:basedOn 元素中 w:val 的值是这个样式继承的其他样式 ID;这个元素很重要,用于实现块级元素的样式继承。w:link 元素中 w:val 的值也是这个样式继承的其他样式 ID,不过这个元素继承的是行内元素的样式。w:pPr 元素设定的是段落样式,其中 w:spacing 元素用于设置段前和段后距离,单位是二十分之一英寸。w:rPr 元素用于设置段落中行内元素的样式,其中 w:rFonts 元素用于设置字体(可以分别设置中西文字体),w:sz 元素用于设置字号,单位是二分之一点(pt)。

docx 文件的样式规则乍看起来很复杂,其实也比较易于理解。我想真正让人觉得难懂的,是 XML 结构。虽然如此,我还是不建议直接手写样式。你可以先在 Word 中通过 GUI 创建样式,然后解压 docx 文件,修改样式,或者把样式复制出来,方便以后重用。

另一个可能需要修改的文件是 fontTable.xml。这个文件定义 docx 文件中用到的所有字体。下面是一个字体定义示例:

<w:font w:name="宋体">
    <w:altName w:val="SimSun"/>
    <w:panose1 w:val="02010600030101010101"/>
    <w:charset w:val="86"/>
    <w:family w:val="auto"/>
    <w:pitch w:val="variable"/>
    <w:sig w:csb0="00040001" w:csb1="00000000" w:usb0="00000003" w:usb1="080E0000" w:usb2="00000010" w:usb3="00000000"/>
</w:font>

字体的定义看起来比样式简单,其实有很多属性的值是使用某种机制生成的,例如 w:panose1 元素的 w:val 属性,因此不要自己动手编写。同样,也是先在 Word 中定义好,然后将其复制出来。

定制方式

如果只需要定制 docx 文件的样式,可以准备好 styles.xmlfontTable.xml 文件,然后替换掉 Pandoc 生成的 docx 文件中的这两个文件。例如,在命令行中可以执行下述命令替换:

zip -r sample.docx word/styles.xml word/fontTable.xml

下面举个定义样式的例子。在一本书中,每一章一般都新起一页,而不是和前一章的内容连在一起。也就是说,新的一章要换页。在 OOXML 中,换页主要由两种方式,这里我们要使用 w:pageBreakBefore 元素。后面再介绍另一种方式。按照 Pandoc 的生成方式,一章的标题一般是一级标题,所以我们可以在一级标题的样式中加入 w:pageBreakBefore 元素,在一级标题之前换页,如下所示:

<w:style w:styleId="Heading1" w:type="paragraph">
    <w:name w:val="Heading 1"/>
    <w:pPr>
      <w:pageBreakBefore/>
    </w:pPr>
</w:style>

加入这个样式后,在 docx 文件的每个一级标题之前都会换页。

过滤器

有时我们需要手动强制换页,这时该怎么做呢?答案是使用过滤器。Pandoc 提供了强大的过滤器机制,方便使用者定制。Pandoc 转换文件的过程如下所示:

        pandoc         filter         pandoc
source --------> JSON --------> JSON --------> target

因此,我们可以使用过滤器修改 Pandoc 生成的 JSON 中间格式。Pandoc 的过滤器有 Python APIPHP APINode/JavaScript API。不过我对这些语言都不熟悉,所以自己写了一个“简陋”的 Ruby API

下面说明怎么使用过滤器实现手动换页。假设我们在 Markdown 文件中输入 <!--PAGEBREAK--> 时是想换页。

#!/usr/bin/env ruby

require 'pandocfilter'

filter = lambda do |key, value, format, meta|
  if key == 'RawBlock' && value[1] == '<!--PAGEBREAK-->'
    xml = %(<w:p><w:r><w:br w:type="page"/></w:r></w:p>)

    return PandocFilter::Node.raw_block('openxml', xml)
  end
end

PandocFilter.process &filter

在这个过滤器中,我们把 <!--PAGEBREAK--> 替换成 OOXML 格式的元素(raw_block 方法创建的是 RawBlock 元素)。这个元素表示一个段落,但没有内容。换页的关键是 w:br 元素,当 w:type 属性的值为 page 时表示换页。

除了前面举的几个例子之外,我还做了其他定制,这里就不再一一介绍了。

总结

虽然我现在基本上实现了所需的 docx 文件样式,但使用 Markdown + Pandoc 的方式还是让我觉得别扭。Pandoc 虽然很强大,相对也易于定制,但是 Markdown 有其局限性。Markdown 是为了在 Web 中快速书写而创造的,不是为了写书稿。写书稿首选当然是 AsciiDoc

如果有时间的话,我会开发一个 AsciiDoc 到 docx 的转换程序。当然,这是一个愿景,要知道,OOXML 规范(ECMA-376)可是超过五千页啊。

~~~ EoF. 感谢阅读!~~~