OpenXml编程--修正Word目录页码错误
场景描述
图1
图1是一个PDF文件生成的简单流程,事先做好的Word模板和数据源进行匹配以生成新的Word文档,然后再将Word文档转换为PDF文档。由Word文档和数据源产生新的Word文档我们采用的是FlexDoc组件(http://flexdoc.codeplex.com/)。生成的PDF文档要求有目录,如图2所示。目录是在Word模板中定义的,并没有采用在代码中自动生成目录的方式,这样是因为可以很方便的更改目录的样式,如图3所示。
图2
图3
生成的Word的页码是不会自动更新的,但是会在转PDF的时候更新,这时候我们遇到了一个FlexDoc的Bug,转换后的目录产生了“未定义书签的错误”。如图4。
图4
本文从Word目录的原理出发,探寻页码转换出错的原因,继而提出完整的解决方案。
Word目录绑定原理
word目录有多种类型,类型是拿什么区别的呢?首先我们插入Word2007中的“自动目录2”,如图5。
图5 插入自动目录2
目录插入成功之后,我们选择目录,右键—>编辑域,切换到域编辑界面,如图6。
图6 编辑域
在域编辑页面在域名项选择TOC,然后单击选项,在选项界面中我们可以看到TOC域支持的开关,不同的开关组合就是不同Word目录,如图7所示。刚才我们选择的“自动目录2”的域代码为TOC \o "1-3" \h \z \u 。关于各个开关的含义,您自己看说明就可以 了,我就不啰嗦了。
图7 编辑域选项
下面我们从WordML的角度继续研究目录。打开word文档,找到Body节点,再找到W:sdt节点,如图8。
图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。
图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文档写在磁盘上之前来修正目录。流程如下:
代码实现
代码很简单,全部代码如下所示:
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。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
关注我: