zoukankan      html  css  js  c++  java
  • OpenXml编程修正Word目录页码错误

    场景描述

    image

    图1

     

            图1是一个PDF文件生成的简单流程,事先做好的Word模板和数据源进行匹配以生成新的Word文档,然后再将Word文档转换为PDF文档。由Word文档和数据源产生新的Word文档我们采用的是FlexDoc组件(http://flexdoc.codeplex.com/)。生成的PDF文档要求有目录,如图2所示。目录是在Word模板中定义的,并没有采用在代码中自动生成目录的方式,这样是因为可以很方便的更改目录的样式,如图3所示。

    image

    图2

    image

    图3

         生成的Word的页码是不会自动更新的,但是会在转PDF的时候更新,这时候我们遇到了一个FlexDoc的Bug,转换后的目录产生了“未定义书签的错误”。如图4。

    image

    图4

            本文从Word目录的原理出发,探寻页码转换出错的原因,继而提出完整的解决方案。

    Word目录绑定原理

          word目录有多种类型,类型是拿什么区别的呢?首先我们插入Word2007中的“自动目录2”,如图5。

    image

    图5  插入自动目录2

    目录插入成功之后,我们选择目录,右键—>编辑域,切换到域编辑界面,如图6。

    image 

    图6  编辑域

           在域编辑页面在域名项选择TOC,然后单击选项,在选项界面中我们可以看到TOC域支持的开关,不同的开关组合就是不同Word目录,如图7所示。刚才我们选择的“自动目录2”的域代码为TOC \o "1-3" \h \z \u 。关于各个开关的含义,您自己看说明就可以 了,我就不啰嗦了。

    image

    图7   编辑域选项

    下面我们从WordML的角度继续研究目录。打开word文档,找到Body节点,再找到W:sdt节点,如图8。

    image

    图8  找到w:sdt节点

    w:sdt节点代表SdtBlock,SdtBlock又是什么呢?就是包在目录外面的那个框,SdtBlock并不是word目录必须的元素,插入自动目录 的时候word默认会将目录放在SdtBlock中,您也可以选择去除,由于SdtBlock可以帮助我们在程序中迅速找到目录项,所以我要去所有的目标中的目录必须带SdtBlock。SdtBlock节点下有一个w:sdtContent (对应的对象为SdtContentBlock)子节点,该子节点下包含了多个w:p(对应的对象为Paragraph)标签,这些w:p标签组成了Word目录。现在我们展开其中一个w:p,看看里面包含了什么秘密。

    代码清单1   一个目录项

       1:  <w:p w:rsidRPr="00F34D5F" w:rsidR="00F34D5F" w:rsidRDefault="00F34D5F" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
       2:    <w:pPr>
       3:      <w:pStyle w:val="20" />
       4:      <w:rPr>
       5:        <w:rFonts w:asciiTheme="minorHAnsi" w:hAnsiTheme="minorHAnsi" w:eastAsiaTheme="minorEastAsia" />
       6:        <w:color w:val="auto" />
       7:      </w:rPr>
       8:    </w:pPr>
       9:    <w:hyperlink w:history="1" w:anchor="_Toc296003347">
      10:      <w:r w:rsidRPr="00F34D5F">
      11:        <w:rPr>
      12:          <w:rStyle w:val="ad" />
      13:          <w:rFonts w:hint="eastAsia" />
      14:          <w:color w:val="auto" />
      15:        </w:rPr>
      16:        <w:t>作答有效性分析</w:t>
      17:      </w:r>
      18:      <w:r w:rsidRPr="00F34D5F">
      19:        <w:rPr>
      20:          <w:webHidden />
      21:          <w:color w:val="auto" />
      22:        </w:rPr>
      23:        <w:tab />
      24:      </w:r>
      25:      <w:r w:rsidRPr="00F34D5F">
      26:        <w:rPr>
      27:          <w:webHidden />
      28:          <w:color w:val="auto" />
      29:        </w:rPr>
      30:        <w:fldChar w:fldCharType="begin" />
      31:      </w:r>
      32:      <w:r w:rsidRPr="00F34D5F">
      33:        <w:rPr>
      34:          <w:webHidden />
      35:          <w:color w:val="auto" />
      36:        </w:rPr>
      37:        <w:instrText xml:space="preserve"> PAGEREF _Toc296003347 \h </w:instrText>
      38:      </w:r>
      39:      <w:r w:rsidRPr="00F34D5F">
      40:        <w:rPr>
      41:          <w:webHidden />
      42:          <w:color w:val="auto" />
      43:        </w:rPr>
      44:      </w:r>
      45:      <w:r w:rsidRPr="00F34D5F">
      46:        <w:rPr>
      47:          <w:webHidden />
      48:          <w:color w:val="auto" />
      49:        </w:rPr>
      50:        <w:fldChar w:fldCharType="separate" />
      51:      </w:r>
      52:      <w:r w:rsidRPr="00F34D5F">
      53:        <w:rPr>
      54:          <w:webHidden />
      55:          <w:color w:val="auto" />
      56:        </w:rPr>
      57:        <w:t>1</w:t>
      58:      </w:r>
      59:      <w:r w:rsidRPr="00F34D5F">
      60:        <w:rPr>
      61:          <w:webHidden />
      62:          <w:color w:val="auto" />
      63:        </w:rPr>
      64:        <w:fldChar w:fldCharType="end" />
      65:      </w:r>
      66:    </w:hyperlink>
      67:  </w:p>

            代码清单1是w:sdtContent 中的一个w:p项内容。现在我们来看里面几个关键项。第9行代码“<w:hyperlink w:history="1" w:anchor="_Toc296003347">”是w:hyperlink(对应的对象为Hyperlink )标记的起始配置,w:hyperlink代表超链接,点击目录会自动跳转到文档中的正确位置,如果您的TOC域支持的开关没有“\h”选项的话是不会产生w:hyperlink标签的,那么您看到的目录项的代码是另一种样子,这里我就不演示了。这里我们重点关注w:anchor属性,该属性指定了超链接的位置。那么w:anchor的值"_Toc296003347"又是什么呢?先不做解释,我们再看另一个标记,第37行的“<w:instrText xml:space="preserve"> PAGEREF _Toc296003347 \h </w:instrText>”,w:instrText(对应的对象为FieldCode)标签的值 “PAGEREF _Toc296003347 \h ”是用来标识超链接的页码的,但是它本身并没有页码值,而是引用了一个位置,最后更新页码的时候会将那个位置所在页的页码赋值给第57行的<w:t>。第50行的<w:fldChar w:fldCharType="separate" />标签是目录项的标题和页码之间的分隔符样式。第16行的“<w:t>作答有效性分析</w:t>”就是当前目录项的标题,实现显示的是word文档正文中的1级 、二级或3级标题。

        现在我们基本了解了目录的组成,还有一个关键的定位属性没有解释,我们继续查看word文档,看下面这一段代码:

    代码2   一个二级标题

       1:  <w:p w:rsidRPr="00F34D5F" w:rsidR="000535A9" w:rsidP="00F34D5F" w:rsidRDefault="00E24DF2" 
    xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
       2:    <w:pPr>
       3:      <w:pStyle w:val="2" />
       4:      <w:ind w:firstLine="372" w:firstLineChars="133" />
       5:      <w:rPr>
       6:        <w:rFonts w:ascii="微软雅黑" w:hAnsi="微软雅黑" w:eastAsia="微软雅黑" w:cstheme="minorBidi" />
       7:        <w:bCs w:val="0" />
       8:        <w:color w:val="93550D" />
       9:        <w:sz w:val="28" />
      10:        <w:szCs w:val="24" />
      11:      </w:rPr>
      12:    </w:pPr>
      13:    <w:bookmarkStart w:name="_Toc295939763" w:id="3" />
      14:    <w:bookmarkStart w:name="_Toc296003347" w:id="4" />
      15:    <w:r w:rsidRPr="00F34D5F">
      16:      <w:rPr>
      17:        <w:rFonts w:hint="eastAsia" w:ascii="微软雅黑" w:hAnsi="微软雅黑" w:eastAsia="微软雅黑" w:cstheme="minorBidi" />
      18:        <w:bCs w:val="0" />
      19:        <w:color w:val="93550D" />
      20:        <w:sz w:val="28" />
      21:        <w:szCs w:val="24" />
      22:      </w:rPr>
      23:      <w:t>作答有效性分析</w:t>
      24:    </w:r>
      25:    <w:bookmarkEnd w:id="3" />
      26:    <w:bookmarkEnd w:id="4" />
      27:  </w:p>
           看代码2所示的内容,实际上是一个二级标题,该二级标题包含在一个单独的<w:p>标记内,从哪里能看出该内容的大纲级别是二级呢?看第3行代码---<w:pStyle w:val="2" />。
    然后我们看第13、1
    4、25和26四行代码,是两对w:bookmarkStart 和bookmarkEnd标签,第14行的w:name="_Toc296003347"是不是很眼熟呢?没错,就是目录项中的定位标记。
         到现在为止,我们已经明白了目录的原理,那么为什么会出错呢?我们看一个出错的Word文档,如图9。
    image
    图9  页码更新出错的Word文档
           看图9中,比较突出是几个w:bookmarkStart 标签,它们本应该是如代码2里那样,和bookmarkEnd标签一起成对的出现在P标签内然后上学包裹标题,但是现在它却单独跑到了P标签外
    ,如果bookmarkEnd标签单独的跑出来也会造成页码更新失败。代码3是标题的内容,我们可以看到只剩下两个孤零零的bookmarkEnd标签。这就是出错的原因。
    <w:p w:rsidRPr="00115C2B" w:rsidR="009E7404" w:rsidP="009A7ED0" w:rsidRDefault="00BD76F7" 
    xmlns:w
    ="http://schemas.openxmlformats.org/wordprocessingml/2006/main"> <w:pPr> <w:pStyle w:val="1" /> <w:jc w:val="center" /> <w:rPr> <w:rFonts w:ascii="微软雅黑" w:hAnsi="微软雅黑" w:cstheme="majorBidi" /> <w:color w:val="365F91" w:themeColor="accent1" w:themeShade="BF" /> <w:kern w:val="0" /> <w:lang w:val="zh-CN" /> </w:rPr> </w:pPr> <w:r w:rsidRPr="00115C2B"> <w:rPr> <w:rFonts w:hint="eastAsia" w:ascii="微软雅黑" w:hAnsi="微软雅黑" w:cstheme="majorBidi" /> <w:color w:val="365F91" w:themeColor="accent1" w:themeShade="BF" /> <w:kern w:val="0" /> <w:lang w:val="zh-CN" /> </w:rPr> <w:t>整体测评结果</w:t> </w:r> <w:bookmarkEnd w:id="2" /> <w:bookmarkEnd w:id="1" /> </w:p>

    修正策略

         问题我们已经分析清楚了,其实这是FlexDoc的bug,当然我们可以通过修改FlexDoc的源代码来解决这个问题,但是我实在是懒得读源码,决定在FlexDoc匹配数据之后将word文档写在磁盘上之前来修正目录。流程如下:

     image

    代码实现

    代码很简单,全部代码如下所示:

       1:   public static void FixtDirectory(WordprocessingDocument wdDoc)
       2:          {
       3:              Body body = wdDoc.MainDocumentPart.Document.Body;
       4:              //获取所有包含一、二级标题的段落
       5:              var parHasStyle = body.Descendants<Paragraph>().Where(t => t.Descendants<ParagraphStyleId>().Count() > 0 && 
    t.Descendants<ParagraphStyleId>().All(c => c.Val == "1" || c.Val == "2"));
       6:              string bookMarkName = "_Toc{0}";
       7:              int num = 988888888;
       8:              Dictionary<string, string> bookMarkAddedDic = new Dictionary<string, string>();
       9:   
      10:              if (parHasStyle.Count() > 0)
      11:              {
      12:                  foreach (Paragraph p in parHasStyle)
      13:                  {
      14:                      var bookmarkEnds = p.Descendants<BookmarkEnd>();//获取段落中所有BookmarkEnd标签
      15:                      var bookmarkStarts = p.Descendants<BookmarkStart>();//获取段落中所有BookmarkStart标签
      16:                      int bookmarkEndsCount = bookmarkEnds.Count();
      17:                      int bookmarkStartsCount = bookmarkStarts.Count();
      18:                      string name = string.Format(bookMarkName, ++num);
      19:                      string id = (num++).ToString();
      20:   
      21:                      //创建新书签用于添加到标题上下
      22:                      BookmarkStart bookmarkStart = new BookmarkStart() { Name = name, Id = id };
      23:                      BookmarkEnd bookmarkEnd = new BookmarkEnd() { Id = id };
      24:   
      25:                      if (bookmarkEndsCount == 0 && bookmarkStartsCount == 0)
      26:                      {
      27:                          if (p.Descendants<Text>().Count() > 0)
      28:                          {
      29:                              AddBookMarkToParagraph(p, bookmarkEnd, bookmarkStart);//添加书签
      30:                              bookMarkAddedDic.Add(p.Descendants<Text>().First().Text, name);//记录添加的书签
      31:                          }
      32:                      }
      33:                      else
      34:                          if (bookmarkEndsCount != bookmarkStartsCount)
      35:                          {
      36:                              DeleteBookMarkFromParagraph(body, p, bookmarkStarts, bookmarkEnds);//删除孤单书签
      37:                              AddBookMarkToParagraph(p, bookmarkEnd, bookmarkStart);//添加新书签
      38:                              string dicKey = GetKey(p);//获取被添加书签的标题
      39:                              bookMarkAddedDic.Add(dicKey, name);//记录添加的书签
      40:                          }
      41:                  }
      42:                  FixtDirectory(bookMarkAddedDic, body);//更新目录
      43:              }
      44:   
      45:          }
      46:   
      47:          /// <summary>
      48:          /// 将段落中文字拼起来得到标题内容
      49:          /// </summary>
      50:          /// <param name="p"></param>
      51:          /// <returns></returns>
      52:          private static string GetKey(Paragraph p)
      53:          {
      54:              return string.Join("", p.Descendants<Text>().Select(t => t.Text));
      55:          }
      56:   
      57:          /// <summary>
      58:          /// 修正书签
      59:          /// </summary>
      60:          /// <param name="bookMarkAddedDic"></param>
      61:          /// <param name="body"></param>
      62:          private static void FixtDirectory(Dictionary<string, string> bookMarkAddedDic, Body body)
      63:          {
      64:              if (bookMarkAddedDic.Count > 0)
      65:              {
      66:                  if (body.Descendants<SdtBlock>().Count() > 0)
      67:                  {
      68:                      //得到SdtContentBlock
      69:                      SdtContentBlock sdtContentBlock = body.Descendants<SdtBlock>().First().GetFirstChild<SdtContentBlock>();
      70:                      //遍历每一个超链接,修改里面的书签值
      71:                      foreach (Hyperlink hyperlink in sdtContentBlock.Descendants<Hyperlink>())
      72:                      {
      73:   
      74:                          Text text = hyperlink.Descendants<Text>().First();//得到目录项绑定的标题内容
      75:                          if (bookMarkAddedDic.Keys.Contains(text.Text))
      76:                          {
      77:                              hyperlink.Anchor = bookMarkAddedDic[text.Text];//超链接绑定到书签的name
      78:                              FieldCode pageRef = hyperlink.Descendants<FieldCode>().First(t => t.Text.Contains("PAGEREF"));//
      79:                              pageRef.Text = "PAGEREF " + hyperlink.Anchor + "\\h";//更新PAGEREF以更新页码
      80:                          }
      81:   
      82:                      }
      83:                  }
      84:   
      85:              }
      86:   
      87:          }
      88:   
      89:          /// <summary>
      90:          /// 删除孤单标签
      91:          /// </summary>
      92:          /// <param name="body"></param>
      93:          /// <param name="p"></param>
      94:          /// <param name="bookmarkStarts"></param>
      95:          /// <param name="bookmarkEnds"></param>
      96:          private static void DeleteBookMarkFromParagraph(Body body, Paragraph p, IEnumerable<BookmarkStart> bookmarkStarts, 
    IEnumerable<BookmarkEnd> bookmarkEnds)
      97:          {
      98:              IEnumerable<BookmarkStart> singleStartElenmentsIn = null;
      99:              IEnumerable<BookmarkEnd> singleEndElenmentsIn = null;
     100:              IEnumerable<BookmarkStart> singleStartElenmentsOut = null;
     101:              IEnumerable<BookmarkEnd> singleEndElenmentsOut = null;
     102:   
     103:              singleStartElenmentsIn = bookmarkStarts.Where(t => 
    !bookmarkEnds.Select(c => c.Id.Value).Contains(t.Id.Value));//获得段落内的孤单BookmarkStart标签
     104:              List<BookmarkStart> bookmarkStartsLst = singleStartElenmentsIn.ToList();
     105:              singleEndElenmentsIn = bookmarkEnds.Where(t => !bookmarkStartsLst.Select(c => c.Id.Value).
    Contains(t.Id.Value));//获得段落内的孤单BookmarkEnd标签
     106:   
     107:              singleStartElenmentsOut = body.Descendants<BookmarkStart>().Where(t => singleEndElenmentsIn.
    Select(c => c.Id.Value).Contains(t.Id.Value));//获得段落外的孤单BookmarkStart标签
     108:              singleEndElenmentsOut = body.Descendants<BookmarkEnd>().Where(t => singleStartElenmentsIn.
    Select(c => c.Id.Value).Contains(t.Id.Value));//获得段落外的孤单BookmarkEnd标签
     109:   
     110:              //删除所有孤单标签
     111:              Remove(singleStartElenmentsOut);
     112:              Remove(singleEndElenmentsOut);
     113:              Remove(singleStartElenmentsIn);
     114:              Remove(singleEndElenmentsIn);
     115:   
     116:          }
     117:   
     118:          private static void Remove(IEnumerable<OpenXmlElement> singleElenments)
     119:          {
     120:              singleElenments.ToList().ForEach(t => t.Remove());//删除标签
     121:          }
     122:   
     123:   
     124:          /// <summary>
     125:          /// 添加新的标签到段落中标题上下
     126:          /// </summary>
     127:          /// <param name="p"></param>
     128:          /// <param name="bookmarkEnd"></param>
     129:          /// <param name="bookmarkStart"></param>
     130:          private static void AddBookMarkToParagraph(Paragraph p, BookmarkEnd bookmarkEnd, BookmarkStart bookmarkStart)
     131:          {
     132:              if (p.Descendants<Text>().Count() > 0)
     133:              {
     134:                  var wtBegin = p.Descendants<Text>().First();
     135:                  var wtEnd = p.Descendants<Text>().Last();
     136:                  Run rBegin = wtBegin.Parent as Run;//得到标题内容开始行
     137:                  Run rEnd = wtEnd.Parent as Run;//得到标题内容结束行
     138:   
     139:                  rBegin.InsertBeforeSelf(bookmarkStart);//在标题上面插入BookmarkStart
     140:                  rEnd.InsertAfterSelf(bookmarkEnd);//在标题下面插入bookmarkEnd
     141:              }
     142:          }
     代码很少,我将说明加在注释上,相信各位都能看的懂。最后还希望大家踊跃留言讨论。谢谢!


    作者:玄魂
    出处:http://www.cnblogs.com/xuanhun/
    原文链接:http://www.cnblogs.com/xuanhun/ 更多内容,请访问我的个人站点 对编程,安全感兴趣的,加qq群:hacking-1群:303242737,hacking-2群:147098303,nw.js,electron交流群 313717550。
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
    关注我:关注玄魂的微信公众号

  • 相关阅读:
    Redis入门
    k8s dubbo微服务之maven配置
    NoSQL发展历史与阿里巴巴架构演进分析
    k8s交付dubbo微服务之部署Jenkins
    k8s版本平滑升级
    读 <The Lost Horizon> 感
    luogu P1026 统计单词个数
    acm一些小细节/技巧
    数据结构与算法——常用高级数据结构及其Java实现
    数据结构与算法——常用排序算法及其Java实现
  • 原文地址:https://www.cnblogs.com/xuanhun/p/2083061.html
Copyright © 2011-2022 走看看