使用officegen 生成word,为word插入可导航的标题
结论
先说结论,写一个styles.xml
内容如下,后续为查找过程
<w:styles xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml"
xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml"
xmlns:w16se="http://schemas.microsoft.com/office/word/2015/wordml/symex" mc:Ignorable="w14 w15 w16se">
<w:style w:type="paragraph" w:styleId="myHeading1">
<w:name w:val="heading 1"/>
</w:style>
<w:style w:type="paragraph" w:styleId="myHeading2">
<w:name w:val="heading 2"/>
</w:style>
<w:style w:type="paragraph" w:styleId="myHeading3">
<w:name w:val="heading 3"/>
</w:style>
<w:style w:type="paragraph" w:styleId="myHeading4">
<w:name w:val="heading 4"/>
</w:style>
<w:style w:type="paragraph" w:styleId="myHeading5">
<w:name w:val="heading 5"/>
</w:style>
</w:styles>
生成程序这样写
let officegen = require("officegen");
let fs = require("fs");
let styleXML = fs.readFileSync("./utils/styles.xml", "utf-8");
let docx = officegen({
type: "docx",
title:"文档一",
styleXML,
});
let style = { bold: true, font_face: "楷体", font_size: 20,align:'left' };
let pObj1 = docx.createP()
pObj1.options.force_style = "myHeading1";
pObj1.addText(`一级标题`,style)
docx.putPageBreak();
let pObj2 = docx.createP()
pObj2.options.force_style = "myHeading2";
pObj2.addText(`二级标题`,style)
docx.putPageBreak();
let pObj3 = docx.createP()
pObj3.options.force_style = "myHeading3";
pObj3.addText(`三级标题`,style)
docx.putPageBreak();
// 将 docx 数据写入到文件
let out = fs.createWriteStream("./out/data.docx");
out.on("error", function (err) {
console.log(err);
});
docx.generate(out);
可以生成带导航的标题
需求
最近接收到一个需求,需要根据Java端传送的JSON数据生成一份Docx格式的报告文档。
技术选择
经过调研,可以使用docxtemplater
、officegen
和adm-zip
来进行Word文档处理。
鉴于文档中包含大量表格且行数、样式等变化较多,docxtemplater
的免费版本不支持动态插入表格,所以排除了此选项。 adm-zip
通过修改Docx的XML来实现,主要操作word/document.xml
,但作为非专业研究人员,大量处理XML的操作还是选择尽量避免。最后选择了officegen
来进行开发。
问题
开发过程中遇到了一个问题,需要对对应文字进行一级标题和二级标题的添加,便于用户通过导航快速跳转
根据officegen文档描述,新增相关代码如下,插入
let officegen = require("officegen");
let fs = require("fs");
// 使用 officegen 创建一个 docx 文件
let styleXML = fs.readFileSync("./utils/styles.xml", "utf-8");
let docx = officegen({
type: "docx",
title:"文档一",
styleXML,
});
let style = { bold: true, font_face: "楷体", font_size: 20,align:'left' };
docx.createP().addText(`一级标题`,style)
docx.putPageBreak();
docx.createP().addText(`二级标题`,style)
docx.putPageBreak();
docx.createP().addText(`三级标题`,style)
// 将 docx 数据写入到文件
let out = fs.createWriteStream("./out/data.docx");
out.on("error", function (err) {
console.log(err);
});
docx.generate(out);
得到了结果如下
发现导航的地方并没有识别标题,并不能快速引导用户跳转到指定章节
寻找解决方案
为了解决这个问题,查询了officegen的在线文档及github的issue,并没有找到对应的解决方案。
仔细想想,既然在word上设置后会生效,docx本质上又是office openxml,那一定有什么配置会让标题的设置生效,那就在word上点一个标题,然后对比一下xml有什么区别,不就能找到了吗。
那就用生成的docx,设置个标题,再设置上标题,一起解压缩下,对比对比,肯定能发现端倪
然后对docx包进行了拆解,将其后缀改为.zip 然后解压缩,解压缩后得到的文件结构如下:
使用排除法,进行范围的文件替换,逐步缩小影响范围,最终发现word/document.xml
和word/styles.xml
这两个文件中被替换后,神奇的导航部分出现了标题。
那就好整了,继续缩小范围,
发现,word/document.xml
有标题版本多这样一个pStyle
的设置
<w:pPr>
<w:pStyle w:val="a3"/>
</w:pPr>
而无标题版本是这样
<w:pPr>
<w:ind/>
</w:pPr>
其中`w:val`对应的值`a3`,在`word/styles.xml`中有对应的配置
<w:style w:type="paragraph" w:styleId="a3">
<w:name w:val="heading 1"/>
<w:basedOn w:val="a"/>
<w:next w:val="a"/>
<w:link w:val="a4"/>
<w:uiPriority w:val="10"/>
<w:qFormat/>
<w:rsid w:val="00C31982"/>
<w:pPr>
<w:spacing w:before="240" w:after="60"/>
<w:jc w:val="center"/>
<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:sz w:val="32"/>
<w:szCs w:val="32"/>
</w:rPr>
</w:style>
<w:style w:type="character" w:customStyle="1" w:styleId="a4">
<w:name w:val="标题 字符"/>
<w:basedOn w:val="a0"/>
<w:link w:val="a3"/>
<w:uiPriority w:val="10"/>
<w:rsid w:val="00C31982"/>
<w:rPr>
<w:rFonts w:asciiTheme="majorHAnsi" w:eastAsiaTheme="majorEastAsia" w:hAnsiTheme="majorHAnsi" w:cstheme="majorBidi"/>
<w:b/>
<w:bCs/>
<w:sz w:val="32"/>
<w:szCs w:val="32"/>
</w:rPr>
</w:style>
替换之后,奇迹发生了!
对内容进行极限删减,并放置在无标题版本的代码中,发现生效的内容是
<w:style w:type="paragraph" w:styleId="a3">
<w:name w:val="heading 1"/>
</w:style>
并且,a3
相当于是一个id
,可以进行修改,只要匹配的上,就可以是任意值,所以可以命名为myHeading1
那么就简单了,只要在程序里,给word/styles.xml
上加入上述代码,理论上就可以完成导航标题的设置
查阅文档,没有发现怎么能够直接插入这样的配置,于是翻阅了github 中 officegen的 issue
在issue中进行检索时候,发现了这样一条:Paragraph styles · Issue #257 · Ziv-Barber/officegen (github.com)
说是添加heading无效,但是有一条评论引起了注意
然后观察了259号issue
可以通过声明docx时候,添加styleXml 的方式添加样式
既然如此,那就尝试一下此方法
先写一个styles.xml
的样式表,命名五个myHeading
(注意:styles.xml
里,第一行是<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
,这一行代码officegen
会为我们添加上,所以样式表里不写这一行,写了会报错)
<w:styles xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml"
xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml"
xmlns:w16se="http://schemas.microsoft.com/office/word/2015/wordml/symex" mc:Ignorable="w14 w15 w16se">
<w:style w:type="paragraph" w:styleId="myHeading1">
<w:name w:val="heading 1"/>
</w:style>
<w:style w:type="paragraph" w:styleId="myHeading2">
<w:name w:val="heading 2"/>
</w:style>
<w:style w:type="paragraph" w:styleId="myHeading3">
<w:name w:val="heading 3"/>
</w:style>
<w:style w:type="paragraph" w:styleId="myHeading4">
<w:name w:val="heading 4"/>
</w:style>
<w:style w:type="paragraph" w:styleId="myHeading5">
<w:name w:val="heading 5"/>
</w:style>
</w:styles>
然后修改生成代码
let officegen = require("officegen");
let fs = require("fs");
let styleXML = fs.readFileSync("./utils/styles.xml", "utf-8");
let docx = officegen({
type: "docx",
title:"文档一",
styleXML,
});
let style = { bold: true, font_face: "楷体", font_size: 20,align:'left' };
docx.createP({
pStypeDef: 'myHeading1'
}).addText(`一级标题`,style)
docx.putPageBreak();
docx.createP({
pStypeDef: 'myHeading1'
}).addText(`二级标题`,style)
docx.putPageBreak();
docx.createP({
pStypeDef: 'myHeading1'
}).addText(`三级标题`,style)
docx.putPageBreak();
// 将 docx 数据写入到文件
let out = fs.createWriteStream("./out/data.docx");
out.on("error", function (err) {
console.log(err);
});
docx.generate(out);
然后执行,发现并未生效
可能有哪里的配置还是不正常,解包后发现,word/styles.xml
被正确放置了,再看word/document.xml
,标题对应的位置,并没有一下代码
<w:style w:type="paragraph" w:styleId="a3">
<w:name w:val="heading 1"/>
</w:style
那可能有别的设置方式,经过各种测试,发现如下写法有效
let pObj1 = docx.createP()
pObj1.options.force_style = "myHeading1";
pObj1.addText(`一级标题`,{ bold: true, font_face: "楷体", font_size: 20,align:'left' })
name修改生成代码如下
let officegen = require("officegen");
let fs = require("fs");
let styleXML = fs.readFileSync("./utils/styles.xml", "utf-8");
let docx = officegen({
type: "docx",
title:"文档一",
styleXML,
});
let style = { bold: true, font_face: "楷体", font_size: 20,align:'left' };
let pObj1 = docx.createP()
pObj1.options.force_style = "myHeading1";
pObj1.addText(`一级标题`,style)
docx.putPageBreak();
let pObj2 = docx.createP()
pObj2.options.force_style = "myHeading2";
pObj2.addText(`二级标题`,style)
docx.putPageBreak();
let pObj3 = docx.createP()
pObj3.options.force_style = "myHeading3";
pObj3.addText(`三级标题`,style)
docx.putPageBreak();
// 将 docx 数据写入到文件
let out = fs.createWriteStream("./out/data.docx");
out.on("error", function (err) {
console.log(err);
});
docx.generate(out);
再进行生成
圆满完成!
转载自:https://juejin.cn/post/7239876138544562233