使用 Open XML SDK 2.0 将 Open XML WordprocessingML 转换为 XHTML

**摘要:**使用 Open XML SDK 将 Open XML WordprocessingML 转换为 XHTML。

上次修改时间: 2011年4月11日

适用范围: Office 2007 | Office 2010 | Open XML | Visual Studio Tools for Microsoft Office | Word 2007 | Word 2010

本文内容
将 WordprocessingML 转换为 XHTML 的好处
使用 HtmlConverter 类
示例:使用不带格式或图像的 HtmlConverter
转换图像
为转换提供 CSS
在转换前接受修订
在转换前简化标记
递归的纯功能转换
其他限制
结论
其他资源

目录

  • 将 WordprocessingML 转换为 XHTML 的好处

  • 使用 HtmlConverter 类

  • 示例:使用不带格式或图像的 HtmlConverter

  • 转换图像

  • 为转换提供 CSS

  • 在转换前接受修订

  • 在转换前简化标记

  • 递归的纯功能转换

  • 其他限制

  • 结论

  • 其他资源

将 WordprocessingML 转换为 XHTML 的好处

将 WordprocessingML 文档转换为 XHTML 有几个令人注目的用途。

  • 您可能希望在应用程序中简单预览 Open XML WordprocessingML 文档。通过转换为 XHTML,您便可使用众多 HTML 查看器之一来集成此功能。

  • 另一个引人注意的应用是与其他软件系统进行互操作,以便可以使用 HTML 实现丰富功能。例如,您可以通过一组字处理文档填充 SharePoint Wiki。

  • 对于开发人员,转换为 XHTML 特别有意义。如果您需要简单查询字处理文档,也许按内容或样式名称选择段落时,编写 XHTML 查询比编写 WordprocessingML 查询要更加简单。

本文介绍的转换器有一些限制。它不能转换样式和字体等格式,但可以转换实际的文本内容和图像。虽然在示例中不会转换源文档的格式,但您可以提供级联样式表,以便为段落样式定义适当的类。该级联样式表随后会提供段落级别的格式。

使用 HtmlConverter 类

若要下载 HtmlConverter 类和示例,请参阅 PowerTools for Open XML(该链接可能指向英文页面),单击"下载"选项卡,然后下载 HtmlConverter.zip。

若要构建 HtmlConverter,还必须下载和安装 Open XML SDK 2.0 for Microsoft Office(该链接可能指向英文页面)

如果您要设置自己的项目来包括 HtmlConverter 类,则需要添加对以下四个程序集的引用:

  • DocumentFormat.OpenXml

  • OpenXmlPowerTools

  • System.Drawing

  • Windows.Base

图 1 显示了用作测试源的 WordprocessingML 文档。

图 1. 测试源 WordprocessingML 文档

Word 文档的屏幕剪辑

在转换为 XHTML 时,该文档如图 2 所示。

图 2. 呈现的 XHTML

Internet Explorer 的屏幕剪辑

该示例的第一个版本具有以下目标。

  • **准确转换文档文本。**将各段落的文本转换为适当的 XHTML 元素:段落元素 (x:p) 或适当的标题元素(例如 x:h1 或 x:h2)。文本转换过程很可靠。如果 WordprocessingML 包含修订跟踪、内容控件或各种注释(如字段),则可准确转换文本。

  • **不能转换格式,例如段落字体或字符字体。**不过,HtmlConverter 类可以有选择地添加各段落或标题元素的类属性(派生自段落样式名称)。您随后可提供级联样式表,以应用段落级别的基本格式。

  • **将 WordprocessingML 表转换为简单的 XHTML 表。**此转换是递归转换,可处理嵌入其他表的单元格中的表。

  • **正确转换超链接。**此转换初看上去更复杂,因为超链接有两种形式的标记。

  • 转换编号列表和点符列表。在 Open XML WordprocessingML 中使用编号列表一文介绍了编号列表和点符列表的标记语义。该文章中的代码包括一个新类(ListItemRetriever 类),它检索任何段落的列表项文本。

  • **正确转换图像。**该问题更难,因为 WordprocessingML 中的图像已嵌入文档包(.zip 文件),但在 XHTML 中,图像有其自己的 URL。您必须将图像写入磁盘上单独的文件中,或者将其上载到服务器,具体取决于您要如何使用 XHTML。显示 XHTML 的浏览器需要访问这些图像。利用 HtmlConverter 类,您可以提供事件处理程序,后者可将图像视为参数,并返回要插入 XHTML 中的标记。在该事件处理程序中,您可以根据需要上载或保存图像,然后返回 XHTML IMG 元素,其中包含指向图像的相应 URI 或 URL。

HtmlConverter 类的最重要目标是提供源代码以及有关如何执行这种转换的指南,并提供灵活的着手点,以使您可以编写符合自己目标的 HTML 转换代码。

示例:使用不带格式或图像的 HtmlConverter

以下示例显示了 HtmlConverter 类的最简单用法。

// This example shows the simplest conversion. No images are converted.
// A cascading style sheet is not used.
byte[] byteArray = File.ReadAllBytes("Test.docx");
using (MemoryStream memoryStream = new MemoryStream())
{
    memoryStream.Write(byteArray, 0, byteArray.Length);
    using (WordprocessingDocument doc =
        WordprocessingDocument.Open(memoryStream, true))
    {
        HtmlConverterSettings settings = new HtmlConverterSettings()
        {
            PageTitle = "My Page Title"
        };
        XElement html = HtmlConverter.ConvertToHtml(doc, settings);

        // Note: the XHTML returned by ConvertToHtmlTransform contains objects of type
        // XEntity. PtOpenXmlUtil.cs defines the XEntity class. See
        // https://blogs.msdn.com/ericwhite/archive/2010/01/21/writing-entity-references-using-linq-to-xml.aspx
        // for detailed explanation.
        //
        // If you further transform the XML tree returned by ConvertToHtmlTransform, you
        // must do it correctly, or entities do not serialize properly.

        File.WriteAllText("Test.html", html.ToStringNewLineOnAttributes());
    }
}

在将 Open XML 源文档转换为 XHTML 之前,HtmlConverter 类可对该源文档执行两次转换。第一次,它接受所有修订。第二次,它会简化标记。因为您不希望将这些更改内容写回源文档;因此,所有示例均使用以下方法,即,将源文档读入字节数组,创建大小可调的内存流,将该字节数组写入内存流,然后从内存流打开字处理文档。有关此内容的详细信息,请参阅博客文章通过先接受修订来简化 Open XML WordprocessingML 查询(该链接可能指向英文页面)

您可以设置各种转换设置,具体方法是:实例化 HtmlConverterSettings 类的实例,在其中设置字段,然后将其传递到 HtmlConverter.ConvertToHtml 方法。Open XML 源文档不一定包含适用于 XHTML 页的页标题的文本。因此,可使用 HtmlConverterSettings 对象指定页标题。

HtmlConverter.ConvertToHtml 方法会返回一个 XElement 对象,其中包含 XHTML。该示例从 PtUtil.cs 模型中使用扩展方法 ToStringNewLineOnAttributes(该方法是在 PtExtensions 类中定义的)来编写 XHTML。此扩展方法可对各属性位于其自己行中的 XML 进行序列化,这样当某个元素包含若干属性时,可以使 XHTML 更易于读取。以下 XML 显示了不同属性,各属性均位于其自己的行中。

<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <meta
      http-equiv="Content-Type"
      content="text/html; charset=windows-1252" />
    <meta
      name="Generator"
      content="PowerTools for Open XML" />
    <title>My Page Title</title>
  </head>
  <body>
    <p>This is a test document to transform from WordprocessingML to XHTM.</p>
    <p />
    <p>Find out more about <A
        href="https://www.codeplex.com/powertools">PowerTools for Open XML</A>.</p>
    . . .
重要说明重要说明

为了正确序列化实体,由 ConvertToHtmlTransform 方法返回的 HTML 需包含类型为 XEntity 的对象。PtOpenXmlUtil.cs 可定义 XEntity 类。有关详细信息,请参阅使用 LINQ to XML 编写实体引用(该链接可能指向英文页面)。如果还要转换由 ConvertToHtmlTransform 方法返回的 XML 树,则必须正确执行此操作,否则实体不能正确进行序列化。

转换图像

较为复杂的示例是将 WordprocessingML 中的图像转换为 XHTML 中的图像。如前所述,图像会直接存储在 Open XML 包中,但在 XHTML 中,必须将图形存储为单独的文件,而该文件包含不同的 URI 或 URL。因此,您必须提供正确存储图像的委托。该委托将传递给 ImageInfo 对象,具体定义如下所示

public class ImageInfo
{
    public Bitmap Bitmap;
    public XAttribute ImgStyleAttribute;
    public string ContentType;
    public XElement DrawingElement;
    public string AltText;

    public static int EmusPerInch = 914400;
    public static int EmusPerCm = 360000;
}

此定义为您提供了决定对图像执行哪些操作所需的全部信息。DrawingElement 字段包含用于 WordprocessingML 图形对象的 XML。

提供该委托的一种便捷方式是提供 lambda 表达式。这允许方法(lambda 表达式)中的代码访问容纳范围中的本地变量。

为委托编写的方法将返回要插入 XHTML 中相应位置的标记。ImgStyleAttribute 对象是包含图像相应大小信息的 XAttribute 对象。它适用于直接添加到您创建的并从委托中返回的 IMG 元素中。例如,它可能包含样式属性 style="width: 0.5in; height: 0.4833333in"。在创建 IMG 元素时,它可能如下所示。

<img
  src="Test_files/image1.jpeg"
  style="width: 0.5in; height: 0.4833333in"
  alt="Picture 1" />

下面的示例创建一个包含图像的子目录,将图像保存到该子目录,然后返回包含图像路径的 IMG 元素。该子目录的名称是文档名称加上追加到名称后的"_files"。为了轻松运行该示例多次,该示例会删除并重新创建目录(如果该目录已存在)。

下面的示例演示如何将包含图像的文档转换为 XHTML。

// This example shows conversion of images. A cascading style sheet is not used.
string sourceDocumentFileName = "Test.docx";
FileInfo fileInfo = new FileInfo(sourceDocumentFileName);
string imageDirectoryName = fileInfo.Name.Substring(0,
    fileInfo.Name.Length - fileInfo.Extension.Length) + "_files";
DirectoryInfo dirInfo = new DirectoryInfo(imageDirectoryName);
if (dirInfo.Exists)
{
    // Delete the directory and files.
    foreach (var f in dirInfo.GetFiles())
        f.Delete();
    dirInfo.Delete();
}
int imageCounter = 0;
byte[] byteArray = File.ReadAllBytes(sourceDocumentFileName);
using (MemoryStream memoryStream = new MemoryStream())
{
    memoryStream.Write(byteArray, 0, byteArray.Length);
    using (WordprocessingDocument doc =
        WordprocessingDocument.Open(memoryStream, true))
    {
        HtmlConverterSettings settings = new HtmlConverterSettings()
        {
            PageTitle = "Test Title",
            ConvertFormatting = false,
        };
        XElement html = HtmlConverter.ConvertToHtml(doc, settings,
            imageInfo =>
            {
                DirectoryInfo localDirInfo = new DirectoryInfo(imageDirectoryName);
                if (!localDirInfo.Exists)
                    localDirInfo.Create();
                ++imageCounter;
                string extension = imageInfo.ContentType.Split('/')[1].ToLower();
                ImageFormat imageFormat = null;
                if (extension == "png")
                {
                    // Convert the .png file to a .jpeg file.
                    extension = "jpeg";
                    imageFormat = ImageFormat.Jpeg;
                }
                else if (extension == "bmp")
                    imageFormat = ImageFormat.Bmp;
                else if (extension == "jpeg")
                    imageFormat = ImageFormat.Jpeg;
                else if (extension == "tiff")
                    imageFormat = ImageFormat.Tiff;

                // If the image format is not one that you expect, ignore it,
                // and do not return markup for the link.
                if (imageFormat == null)
                    return null;

                string imageFileName = imageDirectoryName + "/image" +
                    imageCounter.ToString() + "." + extension;
                try
                {
                    imageInfo.Bitmap.Save(imageFileName, imageFormat);
                }
                catch (System.Runtime.InteropServices.ExternalException)
                {
                    return null;
                }
                XElement img = new XElement(Xhtml.img,
                    new XAttribute(NoNamespace.src, imageFileName),
                    imageInfo.ImgStyleAttribute,
                    imageInfo.AltText != null ?
                        new XAttribute(NoNamespace.alt, imageInfo.AltText) : null);
                return img;
            });

        // Note: the XHTML returned by the ConvertToHtmlTransform method contains objects of type
        // XEntity. PtOpenXmlUtil.cs define the XEntity class. For more information
        // https://blogs.msdn.com/ericwhite/archive/2010/01/21/writing-entity-references-using-linq-to-xml.aspx.

        //
        // If you transform the XML tree returned by the ConvertToHtmlTransform method further, you
        // must do it correctly, or entities do not serialize correctly.

        File.WriteAllText(fileInfo.Directory.FullName + "/" + fileInfo.Name.Substring(0,
            fileInfo.Name.Length - fileInfo.Extension.Length) + ".html",
            html.ToStringNewLineOnAttributes());
    }
}

WordprocessingML 标记可将许多转换应用于图像,例如调整大小、旋转或翻转。该示例没有在存储图像前将旋转或翻转转换应用于该图像。

为转换提供 CSS

您可以配置 HtmlConverter 类来输出每个段落和标题元素的类属性,并可为转换提供 CSS。类属性是段落或标题的样式名称,其中包含作为前缀添加到样式名称中的指定字符串。利用这些类属性,您可以在段落级别配置某些格式。下面的示例在样式名称前面附加"Pt"。然后,样式表为 PtNormal、PtHeading1 和 PtHeading2 定义类。您可以通过此方法相当灵活地配置某些转换样式。

下面的示例显示生成的 XHtml 使用级联样式表的转换。

// This example shows conversion using a cascading style sheet. It also converts images.
string css = @"
        p.PtNormal
            {margin-bottom:10.0pt;
            font-size:11.0pt;
            font-family:""Times"";}
        h1.PtHeading1
            {margin-top:24.0pt;
            font-size:14.0pt;
            font-family:""Helvetica"";
            color:blue;}
        h2.PtHeading2
            {margin-top:10.0pt;
            font-size:13.0pt;
            font-family:""Helvetica"";
            color:blue;}";

string sourceDocumentFileName = "Test.docx";
FileInfo fileInfo = new FileInfo(sourceDocumentFileName);
string imageDirectoryName = fileInfo.Name.Substring(0,
    fileInfo.Name.Length - fileInfo.Extension.Length) + "_files";
DirectoryInfo dirInfo = new DirectoryInfo(imageDirectoryName);
if (dirInfo.Exists)
{
    // Delete the directory and files.
    foreach (var f in dirInfo.GetFiles())
        f.Delete();
    dirInfo.Delete();
}
int imageCounter = 0;
byte[] byteArray = File.ReadAllBytes(sourceDocumentFileName);
using (MemoryStream memoryStream = new MemoryStream())
{
    memoryStream.Write(byteArray, 0, byteArray.Length);
    using (WordprocessingDocument doc =
        WordprocessingDocument.Open(memoryStream, true))
    {
        HtmlConverterSettings settings = new HtmlConverterSettings()
        {
            PageTitle = "Test Title",
           CssClassPrefix = "Pt",
           Css = css,
            ConvertFormatting = false,
        };
        XElement html = HtmlConverter.ConvertToHtml(doc, settings,
            imageInfo =>
            {
                DirectoryInfo localDirInfo = new DirectoryInfo(imageDirectoryName);
                if (!localDirInfo.Exists)
                    localDirInfo.Create();
                ++imageCounter;
                string extension = imageInfo.ContentType.Split('/')[1].ToLower();
                ImageFormat imageFormat = null;
                if (extension == "png")
                {
                   // Convert the .png file to a .jpeg file.
                    extension = "jpeg";
                    imageFormat = ImageFormat.Jpeg;
                }
                else if (extension == "bmp")
                    imageFormat = ImageFormat.Bmp;
                else if (extension == "jpeg")
                    imageFormat = ImageFormat.Jpeg;
                else if (extension == "tiff")
                    imageFormat = ImageFormat.Tiff;

                // If the image format is not one that you expect, ignore it,
                // and do not return markup for the link.
                if (imageFormat == null)
                    return null;

                string imageFileName = imageDirectoryName + "/image" +
                    imageCounter.ToString() + "." + extension;
                try
                {
                    imageInfo.Bitmap.Save(imageFileName, imageFormat);
                }
                catch (System.Runtime.InteropServices.ExternalException)
                {
                    return null;
                }
                XElement img = new XElement(Xhtml.img,
                    new XAttribute(NoNamespace.src, imageFileName),
                    imageInfo.ImgStyleAttribute,
                    imageInfo.AltText != null ?
                        new XAttribute(NoNamespace.alt, imageInfo.AltText) : null);
                return img;
            });

        // Note: The XHTML returned by the ConvertToHtmlTransform method contains objects of type
        // XEntity. PtOpenXmlUtil.cs define the XEntity class. For more information, see
        // https://blogs.msdn.com/ericwhite/archive/2010/01/21/writing-entity-references-using-linq-to-xml.aspx.

        //
        // If you transform the XML tree returned by the ConvertToHtmlTransform method further, you
        // must do it correctly, or entities do not serialize correctly.

        File.WriteAllText(fileInfo.Directory.FullName + "/" + fileInfo.Name.Substring(0,
            fileInfo.Name.Length - fileInfo.Extension.Length) + ".html",
            html.ToStringNewLineOnAttributes());
    }
}

现在,您看到了一些如何使用 HtmlConverter 类的示例,下一节将介绍如何编写转换。

在转换前接受修订

我在将 WordprocessingML 转换为 XHTML 时执行的第一个步骤是接受所有跟踪修订。Accepting Revisions in Open XML Word-Processing Documents一文讨论了跟踪修订的语义。我还在 Microsoft Visual C# 3.0 中发布了接受 CodePlex 跟踪修订的代码示例。若要下载此示例,请参阅 PowerTools for Open XML(该链接可能指向英文页面)。RevisionAccepter 类位于其自己的下载位置,但也包含在 HtmlConverter 示例中。若要下载 RevisionAccepter.zip 或 HtmlConverter.zip,请在 PowerTools for Open XML(该链接可能指向英文页面) 上单击"下载"选项卡。

接受修订可显著降低标记转换的复杂性。有 40 多种元素会使内容处理复杂化,其中许多元素具有复杂的语义。最好先处理这些元素,然后再转换文档内容。

在转换前简化标记

WordprocessingML 包含一个非常丰富的词汇表,其中有许多方面与将内容转换为 XHTML 无关。如果先将文档修改为更简单有效的文档,则编写稳定的转换会更简单。我希望 WordprocessingML 处于理想形式,以便进行 XHTML 转换。

为实现此目的,我编写了一个 MarkupSimplifier 类,其中包含一个 SimplifyMarkup 方法。您可以将打开的 WordprocessingDocument 对象传递到 SimplifyMarkup 方法,当此方法返回时,即会完成文档的简化过程。若要控制简化过程,可创建一个 SimplifyMarkupSettings 对象,初始化该对象,并将其传递到 MarkupSimplifier.SimplifyMarkup 方法。

以下代码段演示如何调用 MarkupSimplifier.SimplifyMarkup 方法。

SimplifyMarkupSettings settings = new SimplifyMarkupSettings
{
    RemoveComments = true,
    RemoveContentControls = true,
    RemoveEndAndFootNotes = true,
    RemoveFieldCodes = false,
    RemoveLastRenderedPageBreak = true,
    RemovePermissions = true,
    RemoveProof = true,
    RemoveRsidInfo = true,
    RemoveSmartTags = true,
    RemoveSoftHyphens = true,
    ReplaceTabsWithSpaces = true,
};
MarkupSimplifier.SimplifyMarkup(wordDoc, settings);

简化的一个方面是处理格式相同的相邻运行之间的合并。通过编辑处理,运行会被任意拆分为多个运行。如果将格式相同的相邻运行合并为单个运行,则会更方便,因为 HtmlConverter 对象随后可将单个运行转换为单个范围。在简化其中某些选项后,MarkupSimplifier 方法可合并格式相同的相邻运行。

若要使 HtmlConverter 对象运行,不一定要删除 Rsid 信息,但在开发或增强此转换过程中,通常要查看源 XML。而删除 Rsid 信息可使该操作更简单。

在 XHTML 转换的第一个版本中,我没有使用内容控件,但我肯定会将其用于以后的转换。

将制表符替换为空格是一个有趣的问题。WordprocessingML 存在刻板的物理制表符问题,而 XHTML 没有此问题。大多数转换会尝试模拟使用空格,但对于该版本,我选择不采用此方法。许多现代文档都使用表而不是物理制表符,并且表以合理方式转换。

递归的纯功能转换

您可以很轻松地使用 HtmlConverter 对象,而无需了解如何编写转换。通过使用可以在 HtmlConverterSettings 参数中指定的选项,您可以执行许多自定义转换。不过,如果要为您自己的方案扩展此代码(可能要大量更改生成的 XHTML,或赋予内容控件特殊意义),则必须了解这些类型的转换。将 WordprocessingML 转换为 XHTML 是以文档为中心的转换,根据定义,以文档为中心的转换是递归转换。使用 LINQ to XML 进行以文档为中心的 XML 转换(该链接可能指向英文页面)探讨了此概念。

HtmlConverter 类是使用递归 C# 技术编写的,将此方法与 XSLT 转换方法对比很有用。正确编写的 XSLT 样式表是一种递归的纯功能 转换窗体。优秀的 XSLT 开发人员可编写无状态的功能代码。有时此代码被称为使用推入 模型编写的 XSLT 样式表。某些开发人员尝试使用提取 模型来解决 XSLT 中的问题:他们编写命令性代码来提取 出 XML,然后生成新 XML。这将导致需要更长时间来调试 XSLT 程序且加大了调试难度。使用 C# 3.0 中的递归技术编写代码与编写单纯的功能性 XSLT 推入 代码相似。

首先要了解如何使用 LINQ 编写纯功能查询。不久前,我编写了一个教程,标题为使用 C# 3.0 中的功能编程技术的查询组合(该链接可能指向英文页面)。如果您尚不熟悉如何编写 LINQ 查询,建议您阅读该教程。

递归的纯功能转换介绍起来很简单。您编写一个方法,其中源文档的每个元素都会通过此方法进行传递(除了根据需要轻松绕过元素以外)。在此方法中,可采用您喜爱的任何方式随意测试元素的标识:您可以测试元素名称、元素的属性、元素后面或前面的内容或者是否存在特定的上级元素。当您匹配所需元素时,将返回一个新元素(或元素组),它是该元素 在新 XML 树中的转换形式。您通常可以编写少许功能构造代码,或者编写一个查询来返回一组替换匹配元素的元素。您还可以返回 null,从而有效删除新 XML 树中的元素。实现 XML 纯功能转换的递归方法(该链接可能指向英文页面)演练了如何创建这些类型的转换。

其他限制

本示例旨在演示一种执行以文档为中心的转换的方法。此外,它还提供有关 WordprocessingML 的某些重要方面的信息,但并不表明它能够完全实现到 HTML 的准确转换。同样,它具有以下限制。

  • 它只能处理从左到右的书写语言。我尚未验证非英语语言。

  • 它只能实现一部分编号系统。

  • 它不能转换数学公式、SmartArt 或 DrawingML 绘图。

  • 它不能转换文本框。

  • 它不能处理合并单元格。

结论

将 WordprocessingML 文本转换为 XHTML 在两种情况下很有用。有时,您希望对 Open XML 字处理文档进行非常有限的文件预览,并且如果您具有 HTML 查看器,那么只需稍做处理便可实现此目的。此外,有时您希望在字处理文档中查询一些特定内容。编写一个极为简单的 XHTML 查询而不是较复杂的 WordprocessingML 查询可能会更轻松。

其他资源

有关详细信息,请参阅以下资源: