安道

博客文章

docx 文件定制指南

我在“翻译时使用的应用”一文中提到,因为最近翻译的一本书要求提供 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)可是超过五千页啊。