本文概述
DOCX格式大约有十亿人使用Microsoft Office, 它是在办公室之间交换文档文件的最流行的事实上的标准。它最接近的竞争对手-ODT格式-仅受Open / LibreOffice和某些开源产品的支持, 使其与标准相差甚远。 PDF格式不是竞争对手, 因为PDF无法编辑, 而且不包含完整的文档结构, 因此只能进行有限的局部更改, 例如水印, 签名等。这就是为什么大多数业务文档都以DOCX格式创建的原因。没有替代它的好选择。
尽管DOCX是一种复杂的格式, 但你可能希望手动解析它以完成更简单的任务, 例如索引, 转换为TXT以及进行其他小的修改。我想为你提供有关DOCX内部结构的足够信息, 因此你不必参考ECMA规范(5, 000页的庞大手册)。
理解格式的最佳方法是使用MSWord创建一个简单的单字文档, 并观察编辑文档如何更改基础XML。在某些情况下, 你可能会遇到DOCX无法在MS Word中正确格式化, 不知道为什么的情况, 或者遇到无法清楚地生成所需格式的实例。准确地了解和理解XML的运行状况将对此有所帮助。
我在协作DOCX编辑器CollabOffice上工作了大约一年, 我想与开发人员社区分享一些知识。在本文中, 我将解释DOCX文件结构, 总结散布在Internet上的信息。本文是介于庞大, 复杂的ECMA规范和当前可用的简单Internet教程之间的中介。你可以在我的github帐户的srcmini-docx项目中找到本文随附的文件。
一个简单的DOCX文件
DOCX文件是XML文件的ZIP存档。如果你创建一个新的空Microsoft Word文档, 请在其中写一个单词” Test”并将其解压缩, 你将看到以下文件结构:
即使我们已经创建了一个简单的文档, Microsoft Word中的保存过程仍以XML格式生成了默认主题, 文档属性, 字体表等。
DOCX内的所有文件都是XML文件, 即使扩展名为” .rels”的文件也是如此。
鸣叫
首先, 让我们删除未使用的内容并关注包含主要文本元素的document.xml。删除文件时, 请确保已从其他xml文件中删除了对该文件的所有关系引用。这是一个代码差异示例, 说明我如何清除对app.xml和core.xml的依赖关系。如果你有任何未解决/丢失的引用, MSWord将认为文件已损坏。
这是我们简化的, 最小的DOCX文档的结构(这是github上的项目):
让我们从顶部开始按文件分类:
_rels / .rels
这定义了告诉MS Word在何处查找文档内容的参考。在这种情况下, 它引用了word / document.xml:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"
Target="word/document.xml"/>
</Relationships>
_rels / document.xml.rels
该文件定义对嵌入在文档内容中的资源(例如图像)的引用。我们的简单文档没有嵌入式资源, 因此关系标签为空:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
</Relationships>
[Content_Types] .xml
[Content_Types] .xml包含有关文档内部媒体类型的信息。由于我们只有文字内容, 因此非常简单:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
<Default Extension="xml" ContentType="application/xml"/>
<Override PartName="/word/document.xml"
ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
</Types>
document.xml
最后, 这是带有文档文本内容的主要XML。为了清楚起见, 我删除了一些名称空间声明, 但是你可以在github项目中找到该文件的完整版本。在该文件中, 你会发现文档中的某些名称空间引用尚未使用, 但是你不应删除它们, 因为MS Word需要它们。
这是我们的简化示例:
<w:document>
<w:body>
<w:p w:rsidR="005F670F" w:rsidRDefault="005F79F5">
<w:r><w:t>Test</w:t></w:r>
</w:p>
<w:sectPr w:rsidR="005F670F">
<w:pgSz w:w="12240" w:h="15840"/>
<w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440" w:header="720" w:footer="720"
w:gutter="0"/>
<w:cols w:space="720"/>
<w:docGrid w:linePitch="360"/>
</w:sectPr>
</w:body>
</w:document>
主节点<w:document>表示文档本身, <w:body>包含段落, 并且嵌套在<w:body>中的是由<w:sectPr>定义的页面尺寸。
<w:rsidR>是可以忽略的属性;由MS Word内部人员使用。
让我们看一个包含三个段落的更复杂的文档。我在Microsoft Word的屏幕截图中用相同的颜色突出显示了XML, 因此你可以看到相关性:
<w:p w:rsidR="0081206C" w:rsidRDefault="00E10CAE">
<w:r> <w:t xml:space="preserve">This is our example first paragraph. It's default is left aligned, and now I'd like to introduce</w:t> </w:r>
<w:r> <w:rPr>
<w:rFonts w:ascii="Arial" w:hAnsi="Arial" w:cs="Arial"/>
<w:color w:val="000000"/>
</w:rPr> <w:t>some bold</w:t>
</w:r>
<w:r> <w:rPr>
<w:rFonts w:ascii="Arial" w:hAnsi="Arial" w:cs="Arial"/>
<w:b/> <w:color w:val="000000"/>
</w:rPr> <w:t xml:space="preserve"> text</w:t>
</w:r>
<w:r> <w:rPr> <w:rFonts w:ascii="Arial"
w:hAnsi="Arial" w:cs="Arial"/> <w:color w:val="000000"/> </w:rPr>
<w:t xml:space="preserve">, </w:t>
</w:r>
<w:proofErr w:type="gramStart"/>
<w:r> <w:t xml:space="preserve">and also change the</w:t> </w:r>
<w:r w:rsidRPr="00E10CAE"> <w:rPr><w:rFonts w:ascii="Impact" w:hAnsi="Impact"/>
</w:rPr> <w:t>font style</w:t> </w:r>
<w:r>
<w:rPr> <w:rFonts w:ascii="Impact" w:hAnsi="Impact"/> </w:rPr> <w:t xml:space="preserve"> </w:t>
</w:r>
<w:r> <w:t>to 'Impact'.</w:t></w:r>
</w:p>
<w:p w:rsidR="00E10CAE" w:rsidRDefault="00E10CAE"> <w:r> <w:t>This is new paragraph.</w:t> </w:r></w:p>
<w:p w:rsidR="00E10CAE" w:rsidRPr="00E10CAE" w:rsidRDefault="00E10CAE">
<w:r> <w:t>This is one more paragraph, a bit longer.</w:t> </w:r>
</w:p>
段落结构
一个简单的文档由段落组成, 一个段落由运行组成(一系列具有相同字体, 颜色等的文本), 而运行则由字符组成(例如<w:t>)。<w:t>标记可能具有里面有几个字符, 同一轮中可能有几个。
同样, 我们可以忽略<w:rsidR>。
文字属性
基本文本属性包括字体, 大小, 颜色, 样式等。大约有40个用于指定文本外观的标签。如你在我们的三段示例中所见, 每个运行在<w:rPr>内都有自己的属性, 指定<w:color>, <w:rFonts>和粗体度<w:b>。
需要注意的重要一点是, 属性区分普通和复杂脚本(例如阿拉伯语)这两组字符, 并且属性会根据所影响的字符类型而具有不同的标记。
大多数普通脚本属性标签都具有匹配的复杂脚本标签, 并带有添加的” C”, 以指定该属性用于复杂脚本。例如:<w:i>(斜体)变为<w:iCs>, 普通脚本的<w:b>粗体标签对于复杂脚本变为<w:bCs>。
款式
Microsoft Word中有一个完整的工具栏, 专门用于样式:普通, 无间距, 标题1, 标题2, 标题等等。这些样式存储在/word/styles.xml中(请注意:在我们的简单示例的第一步中, 我们从DOCX中删除了该XML。创建一个新的DOCX来查看)。
将文本定义为样式后, 你将在段落属性标签<w:pPr>中找到对该样式的引用。这是一个示例, 其中我以标题1的样式定义了文本:
<w:p>
<w:pPr>
<w:pStyle w:val="Heading1"/>
</w:pPr>
<w:r>
<w:t>My heading 1</w:t>
</w:r>
</w:p>
这是来自styles.xml的样式本身:
<w:style w:type="paragraph" w:styleId="Heading1">
<w:name w:val="heading 1"/>
<w:basedOn w:val="Normal"/>
<w:next w:val="Normal"/>
<w:link w:val="Heading1Char"/>
<w:uiPriority w:val="9"/>
<w:qFormat/>
<w:rsid w:val="002F7F18"/>
<w:pPr>
<w:keepNext/>
<w:keepLines/>
<w:spacing w:before="480" w:after="0"/>
<w:outlineLvl w:val="0"/>
</w:pPr>
<w:rPr>
<w:rFonts w:asciiTheme="majorHAnsi" w:eastAsiaTheme="majorEastAsia" w:hAnsiTheme="majorHAnsi"
w:cstheme="majorBidi"/>
<w:b/>
<w:bCs/>
<w:color w:val="365F91" w:themeColor="accent1" w:themeShade="BF"/>
<w:sz w:val="28"/>
<w:szCs w:val="28"/>
</w:rPr>
</w:style>
<w:style / w:rPr / w:b> xpath指定字体为粗体, 而<w:style / w:rPr / w:color>表示字体颜色。 <w:basedOn>指示MSWord对任何缺少的属性使用”常规”样式。
财产继承
文本属性是继承的。运行具有自己的属性(w:p / w:r / w:rPr / *), 但它也继承了段落(w:r / w:pPr / *)的属性, 并且两者都可以从/中引用样式属性word / styles.xml。
<w:r>
<w:rPr>
<w:rStyle w:val="DefaultParagraphFont"/>
<w:sz w:val="16"/>
</w:rPr>
<w:tab/>
</w:r>
段落和运行从默认属性开始:w:styles / w:docDefaults / w:rPrDefault / *和w:styles / w:docDefaults / w:pPrDefault / *。要获得角色属性的最终结果, 你应该:
- 使用默认的运行/段落属性
- 附加运行/段落样式属性
- 附加本地运行/段落属性
- 将结果运行属性附加到段落属性上
当我说”将B附加到A”时, 我的意思是遍历B的所有属性并覆盖所有A的属性, 而所有不相交的属性保持不变。
默认属性可能位于的另一个位置是<w:style>标记中的w:type =” paragraph”和w:default =” 1″。请注意, 运行中的字符本身永远不会具有默认样式, 因此<w:style w:type =” character” w:default =” 1″>实际上不会影响任何文本。
鸣叫
1554402290400-dbb29eef3ba6035df7ad726dfc99b2af.png)
运行中的字符可以从其段落继承, 并且两者都可以从styles.xml继承。
切换属性
其中一些属性是”切换”属性, 例如<w:b>(粗体)或<w:i>(斜体);这些属性的行为类似于XOR运算符。
这意味着, 如果父样式为粗体, 子运行样式为粗体, 则结果将是常规的非粗体文本。
你必须进行大量测试和反向工程才能正确处理切换属性。查看ECMA-376 Open XML规范的17.7.3段, 以获取有关切换属性/的正式, 详细的规则/
切换属性是布局器正确处理的最复杂的属性。
鸣叫
字型
字体遵循与其他文本属性相同的通用规则, 但是字体属性的默认值在单独的主题文件中指定, 该主题文件在word / _rels / document.xml.rels下引用, 如下所示:
<Relationship Id="rId7" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="theme/theme1.xml"/>
根据上述参考, 默认字体名称将在word / theme / themes1.xml中的<a:theme>标记, a:themeElements / a:fontScheme / a:majorFont或a:minorFont标记内找到。
除非缺少w:docDefaults / w:rPrDefault标记, 否则默认字体大小为10, 然后为11。
文字对齐
文本对齐由<w:jc>标记指定, 该标记具有四个可用的w:val模式:”左”, “中心”, “右”和”两者”。
” left”是默认模式;文本从段落矩形的左侧开始(通常为页面宽度)。 (本段在左侧对齐, 这是标准的。)
可以预计, “居中”模式会将所有字符居中在页面宽度内。 (同样, 本段示例居中对齐。)
在”右”模式下, 段落文本与右页边距对齐。 (注意此文本如何与右侧对齐。)
“双向”模式在单词之间留出额外的间距, 以便使行变宽并占据整个段落宽度, 但最后一行保持对齐。 (此段说明了这一点。)
图片
DOCX支持两种图像:内联图像和浮动图像。
内联图像与其他字符一起出现在段落内, 使用<w:drawing>而不是使用<w:t>(文本)。你可以使用以下xpath语法找到图像ID:
w:绘图/ wp:内联/ a:图形/ a:graphicData / pic:pic / pic:blipFill / a:blip / @ r:embed
图像ID用于在word / _rels / document.xml.rels文件中查找文件名, 并且应指向word / media子文件夹中的gif / jpeg文件。 (请参阅github项目的word / _rels / document.xml.rels文件, 你可以在其中查看图像ID。)
浮动图像是相对于段落放置的, 文字在它们周围流动。 (这是带有浮动图像的github项目示例文档。)
浮动图片使用<wp:anchor>而不是<w:drawing>, 因此, 如果你删除<w:p>中的任何文本, 请不要使用锚点, 如果你不想删除图片。
MS Word的图像选项将图像对齐称为”文本换行模式”。
桌子
表格的XML标签类似于HTML表格标记–与<table>相同, 与<tr>匹配, 依此类推。
表本身的<w:tbl>具有表属性<w:tblPr>, 每个列属性由<w:tblGrid>中的<w:gridCol>表示。行作为<w:tr>标记一个接一个地跟随, 并且每一行应具有与<w:tblGrid>中指定的相同的列数:
<w:tbl>
<w:tblPr>
<w:tblW w:w="5000" w:type="pct" />
</w:tblPr>
<w:tblGrid><w:gridCol/><w:gridCol/></w:tblGrid>
<w:tr>
<w:tc><w:p><w:r><w:t>left</w:t></w:r></w:p></w:tc>
<w:tc><w:p><w:r><w:t>right</w:t></w:r></w:p></w:tc>
</w:tr>
</w:tbl>
表格列的宽度可以在<w:tblW>标记中指定, 但如果你未定义, 则Word会使用其内部算法来找到最小有效表格尺寸的最佳列宽度。
单位
DOCX内的许多XML属性都指定大小或距离。它们是XML中的整数, 但是它们都有不同的单位, 因此需要进行一些转换。这个主题很复杂, 因此, 我建议Lars Corneliussen撰写这篇有关DOCX文件中单位的文章。他提供的表格很有用, 尽管打印错误较小:英寸应为pt / 72, 而不是pt * 72。
这是备忘单:
常见的DOCX XML单位转换 | ||||||
20分 | 点数DXA / 20 | 英寸pt / 72 | 厘米in * 2, 54 | 字体一半大小pt / 144 | 动车组* 914400 | |
例子 | 11906 | 595.3 | 8, 27… | 21.00086… | 4, 135 | 7562088 |
使用此标签 | pgSz / pgMar / w:spacing | 以s | wp:extent, a:ext |
实施版面设计的技巧
如果要转换DOCX文件(例如, 转换为PDF), 在画布上绘制该文件或计算页数, 则必须实现一个布局器。布局器是一种用于从DOCX文件计算字符位置的算法。
如果需要100%保真度渲染, 这是一项复杂的任务。实施一个好的版面设计器所需的时间以人年为单位, 但是如果你只需要一个简单的有限的版面, 则可以相对快速地完成。
布局器会填充父矩形, 该矩形通常是页面的矩形。它逐个添加单词。当前行溢出时, 它将开始新的一条。如果段落对于父矩形而言过高, 则将其包裹到下一页。
如果你决定实施布局器, 请牢记以下重要事项:
- 布局器应注意文本对齐和图像上的文本浮动
- 它应该能够处理嵌套对象, 例如嵌套表
- 如果要为此类图像提供全面支持, 则必须实施至少两次通过的布局程序, 第一步收集浮动图像的位置, 第二步用文本字符填充空白区域。
- 注意缩进和间距。每个段落之前和之后都有空格, 这些数字由w:spacing标签指定。垂直间距由w:after和w:before标签指定。请注意, 行间距由w:line指定, 但这不是人们期望的行尺寸。要获得行的大小, 请采用当前字体的高度, 乘以w:line并除以12。
- DOCX文件不包含有关分页的信息。除非你计算出每一行需要多少空间以确定页数, 否则你将找不到文档中的页数。如果你需要在页面上找到每个字符的确切坐标, 请确保考虑所有间距, 缩进和大小。
- 如果实现用于处理表的功能齐全的DOCX布局器, 请注意表跨越多个页面时的特殊情况。导致页面溢出的单元也影响其他单元。
- 创建用于计算表列宽度的最佳算法是一个具有挑战性的数学问题, 并且文字处理器和布局器通常使用一些次优的实现。我建议使用W3C HTML表文档中的算法作为第一个近似值。我还没有找到MS Word使用的算法的描述, 并且Microsoft随时间对算法进行了微调, 因此不同版本的Word可能会略有不同地布置表格。
如果不清楚, 请对XML进行反向工程!
当不清楚XML标记在MS Word中的工作方式时, 有两种主要的解决方法:
-
逐步创建所需的内容。从一个简单的docx文件开始。将每个步骤保存到自己的文件中, 例如1.docx, 2.docx。解压缩它们中的每一个, 并使用可视化差异工具进行文件夹比较, 以查看更改后出现的标签。 (对于商业选项, 请尝试Araxis Merge;对于免费选项, 请尝试WinMerge。)
-
如果你生成MS Word不喜欢的DOCX文件, 请向后处理。逐步简化你的XML。在某些时候, 你将了解MS Word发现哪些更改不正确。
DOCX非常复杂, 不是吗?
这很复杂, 而且微软的许可证禁止在服务器端使用MS Word处理DOCX –这是商业产品的相当标准。但是, Microsoft提供了XSLT文件来处理大多数DOCX标签, 但它不能为你提供100%甚至99%的保真度。不支持诸如在图像上换行文本的过程, 但是你将能够支持大多数文档。 (如果你不需要复杂性, 请考虑使用Markdown作为替代。)
如果你有足够的预算(没有免费的DOCX渲染引擎), 则可能要使用商业产品, 例如Aspose或docx4j。最受欢迎的免费解决方案是LibreOffice, 用于在DOCX和其他格式(包括PDF)之间进行转换。不幸的是, LibreOffice在转换期间包含许多小错误, 并且由于它是一种复杂的开源C ++产品, 因此修复保真度问题的速度缓慢且困难。
另外, 如果你发现DOCX布局过于复杂而无法实现自己, 则还可以将其转换为HTML并使用浏览器进行呈现。你还可以考虑使用srcmini的自由XML开发人员之一。
DOCX资源以供进一步阅读
- ECMA DOCX规范
- 用于C#进行DOCX操作的OpenXML库。它不包含有关布局或渲染代码的信息, 但提供了与DOCX中每个可能的XML节点匹配的类层次结构。
- 你始终可以使用诸如docx4j, OpenXML和docx之类的关键字在stackoverflow上进行搜索或询问;社区中有一些知识渊博的人。
评论前必须登录!
注册