java使用Aspose.word保存word更新目录页码报错、页码不对以及样式错乱解决
我使用的aspose-words版本为23.1
保存文件之前,使用aspose.word中的这个方法:
Document.updateFields()
目录格式出现错乱、省略号跑到前面、页码从某一页开始比实际多出一页或几页、页码全部变成一样:


更新域时会更新目录,但是页码可能会有偏差,原因是无法保证域的更新顺序,目录可能不是最后一个更新的,而在更新其他域时导致页码再次发生变化。而且这个更新方法不止会更新页码,还会导致样式和更新前发生改变。在更新文档域之前,我们首先需要更新一次初始的页码,调整为正确的页码,因为文件里面原有的页码可能就是错误的,最后可能出现页码不准确的情况。
这些问题是属于aspose-words的内部bug,在10.x的版本就存,过了n年之后,才在24.2版本修复,但是我升级之后进行验证,发现只解决了一部分问题,还是存在上面所说的省略号和页码全部一样的bug。
下面是24.2版本我使用的代码,保存文件流之前,我们可以自己去写更新域的逻辑,并且进行一些差错处理:
package com.zhou.wordreport.util;
import com.aspose.words.*;
import lombok.Getter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.log4j.Logger;
import java.awt.*;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.*;
import java.util.regex.Pattern;
/**
* word书签填充工具
* @author lang.zhou
* @since 2019/9/11
*/
@Slf4j
public class AsposeDocument extends Document{
private static final Logger LOGGER= Logger.getLogger(AsposeDocument.class);
private static final Pattern p = Pattern.compile("^.*NTP_(\\d)+$");
private static final int DOCX=SaveFormat.DOCX;
private static final int DOC=SaveFormat.DOC;
private static final int PDF=SaveFormat.PDF;
static {
//兼容linux字体
readFont();
}
@Getter
DocumentBuilder builder = null;
public AsposeDocument(String pathName) throws Exception {
this(FileUtils.getFile(pathName));
}
public AsposeDocument(File file) throws Exception {
this(FileUtils.readFileToByteArray(file));
}
public AsposeDocument(byte[] b) throws Exception {
this(new ByteArrayInputStream(b));
}
public AsposeDocument(InputStream b) throws Exception {
super(b);
builder = new DocumentBuilder(this);
}
public static void readFont(){
try{
String fontPath = "/home/aspose/fonts";
//保存为pdf时,linux系统有缺少字体,会出现中文乱码情况,读取从windows系统拷贝的字体,和linux系统本身的字体
FontSettings.getDefaultInstance().setFontsFolders(new String[]{fontPath,"/usr/share/fonts"},false);
}catch (Exception e){
LOGGER.error("读取字体文件失败,生成PDF时可能出现中文乱码!",e);
}
}
public AsposeDocument() throws Exception {
super();
builder = new DocumentBuilder(this);
}
public Set<String> getAllBookmark(){
Set<String> r = new HashSet<>();
BookmarkCollection collection = this.getRange().getBookmarks();
for (Bookmark bookmark : collection) {
r.add(bookmark.getName());
}
return r;
}
public static void main(String[] args) throws Exception {
new AsposeLicense().validate();
AsposeDocument util = new AsposeDocument("C:\\Users\\zhou\\Desktop\\目录更新异常的word.docx");
util.saveFile("C:\\Users\\zhou\\Desktop\\1.docx");
//util.saveFile("C:\\Users\\zhou\\Desktop\\1.pdf");
}
public void saveFile(String outputPath) throws Exception {
int saveFormat = DOCX;
String t=outputPath.toUpperCase(Locale.ENGLISH);
if(t.endsWith(".DOC")){
saveFormat = DOC;
}else if(t.endsWith(".PDF")){
saveFormat = PDF;
}
saveFile(outputPath,saveFormat);
}
/**
* 按格式保存文件
* @param outputPath 输出路径
* @param saveFormat 文件格式
*/
public void saveFile(String outputPath,int saveFormat) throws Exception {
SaveOptions options = SaveOptions.createSaveOptions(saveFormat);
this.update();
outputPath = outputPath.replace("..","");
File newFile = FileUtils.getFile(outputPath);
FileOutputStream fos = null;
try{
fos = FileUtils.openOutputStream(newFile);
this.save(fos,options);
fos.flush();
}finally {
IOUtils.closeQuietly(fos);
}
}
/**
* 如果页码是错的,先修正为正确的页码
*/
@SneakyThrows
private void fitInitPageNumber(FieldCollection fields){
for (int i = 0; i < fields.getCount(); i++) {
Field field = fields.get(i);
if(field.getType() == FieldType.FIELD_TOC){
FieldToc fieldToc = (FieldToc) field;
fieldToc.updatePageNumbers();
}else if(field.getType() == FieldType.FIELD_PAGE_REF){
FieldPageRef ref = (FieldPageRef) field;
ref.update(false);
}
}
}
@SneakyThrows
private boolean fitInitTocRef(FieldCollection fields){
//FieldType.FIELD_PAGE_REF => 目录中的每一个标题,这些标题如果设置成目录,则标题必须添加_Toc开头的隐藏标签(设置目录会自动添加标签),才能成功更新目录
//否则会报错“bookmark not defined”
//预处理所有目录标题,添加_Toc标签
Set<String> abs = this.getAllBookmark();
boolean isFixed = false;
for (int i = 0; i < fields.getCount(); i++) {
Field field = fields.get(i);
if(field.getType() == FieldType.FIELD_PAGE_REF){
FieldPageRef pageRef = (FieldPageRef) field;
String name = pageRef.getBookmarkName();
if (name.startsWith("_Toc")) {
if(!abs.contains(name)){
if(!isFixed){
log.warn("目录书签缺失,将进行自动修正");
}
isFixed = true;
//如果没有_Toc标签,手动添加一个
//log.info("{}-{}",name,pageRef.getResult());
//log.info("给【{}】添加标签:{}",pageRef.getResult(),name);
try{
builder.moveTo(pageRef.getStart());
builder.startBookmark(name);
builder.moveTo(pageRef.getEnd());
builder.endBookmark(name);
}catch (Exception e){
log.warn("给【{}】添加标签失败:{}",name,e.getMessage());
}
abs.add(name);
}
}
}
}
return isFixed;
}
/**
* 处理目录页码可能全部变成一样
*/
@SneakyThrows
private boolean isPageNumberSame(List<FieldPageRef> pageRefList){
//目录页码可能全部变成一样
String firstIndex = null;
String lastIndex = null;
for (int i = 0,j = pageRefList.size(); i < j; i++) {
FieldPageRef fieldPage = pageRefList.get(i);
if(i == 0){
firstIndex = fieldPage.getResult();
}
if(i == j - 1){
lastIndex = fieldPage.getResult();
}
}
if(Objects.equals(firstIndex, lastIndex)){
log.warn("模板目录异常,页码可能全部为{}",firstIndex);
return true;
}
return false;
}
/**
* 处理bookmark not defined的错误,但目录样式会改变
*/
@SneakyThrows
private boolean isTocBookmarkError(){
if (this.getRange().getText().toLowerCase(Locale.ENGLISH).contains("error! bookmark not defined")) {
log.warn("目录更新页码时出错:目录中的标题的标签丢失,尝试更新整个文档。请重新生成模板文件的目录保证此错误不会出现");
return true;
}
return false;
}
/**
* 更新文档域以及目录页码
*/
@SneakyThrows
public void update(){
//添加警告监听器
FontSubstitutionWarningCollector callback = new FontSubstitutionWarningCollector();
this.setWarningCallback(callback);
//this.updateFields();
FieldCollection fields = this.getRange().getFields();
try{
//修复目录引用书签
boolean isFixed = this.fitInitTocRef(fields);
//如果页码是错的,先修正为正确的页码
this.fitInitPageNumber(fields);
List<FieldToc> tocList = new ArrayList<>(fields.getCount());
List<FieldPageRef> pageRefList = new ArrayList<>(tocList.size());
//先更新文档全局
for (int i = 0; i < fields.getCount(); i++) {
Field field = fields.get(i);
if(field.getType() == FieldType.FIELD_TOC){
FieldToc fieldToc = (FieldToc) field;
tocList.add(fieldToc);
if(isFixed){
field.update();
}
}else if(field.getType() == FieldType.FIELD_PAGE_REF){
FieldPageRef pageRef = (FieldPageRef) field;
pageRefList.add(pageRef);
field.update();
}else{
field.update();
}
}
//最后更新页数
for (FieldToc fieldToc : tocList) {
fieldToc.updatePageNumbers();
}
//处理目录页码异常
if(this.isPageNumberSame(pageRefList) || this.isTocBookmarkError()){
this.updateFields();
}
log.debug("文件页数:{}",this.getPageCount());
}catch (Exception e){
log.error("自动修复目录时发生错误:",e);
this.updateFields();
}
}
}
package com.zhou.wordreport.util;
import com.aspose.words.IWarningCallback;
import com.aspose.words.WarningInfo;
import com.aspose.words.WarningType;
import lombok.extern.slf4j.Slf4j;
/**
* 收集并打印word文档中不存在的字体
* @author lang.zhou
* @since 2024/2/18 12:38
*/
@Slf4j
public class FontSubstitutionWarningCollector implements IWarningCallback {
public void warning(WarningInfo info) {
if (info.getWarningType() == WarningType.FONT_SUBSTITUTION){
//FontSubstitutionWarnings.warning(info);
log.warn("字体未找到:" + info.getDescription());
}/*else{
log.warn(info.getDescription());
}*/
}
//public WarningInfoCollection FontSubstitutionWarnings = new WarningInfoCollection();
}
注意:目录里面的每个标题,在文档中都会有一个引用, 这些引用都必须加上一个_Toc开头的隐藏书签,否则目录页码无法正常更新,手动更新目录时会出现"error! bookmark not defined"或者"标签未定义"的错误。
最近又遇到按上面的代码生成文件后,仍然出现目录中的页码比实际页码大,但是生成pdf时页码正常,经过对比word和pdf文件的内容,发现是由于word文件中存在表格,而表格刚好跨行了,设置的允许断行显示,所以实际占用的页面会减少,这个问题应该是aspose在计算目录页码时,没有考虑表格的跨页断行属性的原因。
经过验证,在表格属性里面把表格行设置不允许跨页显示就正常了

也可以在代码里面手动设置:
for (Row row : table.getRows()) {
//禁止行跨页断行
row.getRowFormat().setAllowBreakAcrossPages(false);
}
官方在2024年发布了新的版本,明确写了解决了目录页码错误的问题

上面代码无法解决问题,可以尝试升级到24.2版本试试,我升级之后问题就解决了 。jar包下载地址:
Aspose Repository Browser /java/repo/com/aspose/aspose-words/24.2/
还有另一个可能导致页码错误的原因:
使用的是wps打开的word,而在下面这种情况下,wps和office的兼容性问题:
一页中的内容刚好占满这一页,但是后面又有个分页符时,wps和office对这个字符的渲染处理逻辑有区别,office中会多产生一个空白页,分页符会跑到下一个空白页的开头,个人推测是因为分页符在office中会占布局,所以在office打开word看到的页码是对的。
wps展示时特殊处理了这个空白页,这就导致office中展示是正常的,因为这个空白页可见,而wps中少了一页,因为空白页隐藏了。这个aspose插件是外国人开发的,兼容性肯定以office为标准。
如果场景允许的话,可以特殊处理去掉这个分页符,代码逻辑大致如下:
/**
* 按格式保存文件
* @param outputPath 输出路径
* @param saveFormat 文件格式
*/
public void saveFile(String outputPath,int saveFormat) throws Exception {
SaveOptions options = SaveOptions.createSaveOptions(saveFormat);
try {
// 移除页面开头的分页符,避免 WPS 和 Office 页码不一致
this.removePageBreaksAtPageEnd();
}catch (Exception e){
log.info("处理文档分页符失败", e);
}
this.update();
outputPath = outputPath.replace("..","");
File newFile = FileUtils.getFile(outputPath);
FileOutputStream fos = null;
try{
fos = FileUtils.openOutputStream(newFile);
this.save(fos,options);
fos.flush();
}finally {
IOUtils.closeQuietly(fos);
}
}
/**
* 移除位于页面开头的分页符
*
* 问题说明:
* - Office:页面开头的分页符会产生一个空白页
* - WPS:页面开头的分页符会被忽略,不产生新页面
* - 结果:导致两个软件中目录页码不一致
*
* 解决方案:
* 遍历所有分页符,判断分页符是否位于其所在页面的开头,如果是则删除,否则保留
*
* 判断逻辑:
* 使用LayoutEnumerator遍历页面布局,获取分页符所在页面的第一个内容位置,
* 判断分页符是否在页面开始的第一个内容之后(即页面开头)
*/
@SneakyThrows
public void removePageBreaksAtPageEnd() {
this.updatePageLayout();
final String PAGE_BREAK = "\f";
final String FORM_FEED = "\u000c";
int removedCount = 0;
// 创建布局收集器
LayoutCollector layoutCollector = new LayoutCollector(this);
// 收集所有包含分页符的Run节点
List<Run> pageBreakRuns = new ArrayList<>();
// 遍历文档中所有的Run节点,找出包含分页符的
NodeCollection allRuns = this.getChildNodes(NodeType.RUN, true);
for (int i = 0; i < allRuns.getCount(); i++) {
Run run = (Run) allRuns.get(i);
String text = run.getText();
if (text != null && text.contains(PAGE_BREAK)) {
pageBreakRuns.add(run);
}
}
// 从后向前处理分页符,避免删除影响索引
List<Integer> removedPages = new ArrayList<>();
for (int i = pageBreakRuns.size() - 1; i >= 0; i--) {
Run run = pageBreakRuns.get(i);
// 检查分页符前面的节点是否和它在同一页
int pageNum = shouldRemovePageBreak(run, layoutCollector);
if (pageNum > 0) {
String text = run.getText();
String newText = text.replace(PAGE_BREAK, "").replace(FORM_FEED, "");
if (newText.isEmpty()) {
Paragraph parentPara = (Paragraph) run.getParentNode();
if(parentPara != null && (parentPara.getText().equals(PAGE_BREAK) || parentPara.getText().equals("\f\r"))){
parentPara.remove();
}else{
run.remove();
if (parentPara != null && !parentPara.hasChildNodes()) {
parentPara.remove();
}
}
} else {
run.setText(newText);
}
removedCount++;
removedPages.add(pageNum);
log.debug("已移除第 {} 页的分页符", pageNum);
}
}
if (removedCount > 0) {
this.updatePageLayout();
Collections.sort(removedPages);
log.info("共移除 {} 个分页符,所在页码:{}", removedCount, removedPages);
}
}
/**
* 判断是否应该删除分页符
*
* 逻辑:找到分页符前面的有内容节点,判断是否和分页符在同一页
* 如果不在同一页,删除分页符
*
* @param pageBreakRun 包含分页符的Run节点
* @param layoutCollector 布局收集器
* @return 如果应该删除,返回页码;否则返回0
*/
@SneakyThrows
private int shouldRemovePageBreak(Run pageBreakRun, LayoutCollector layoutCollector) {
final String PAGE_BREAK = "\f";
final String FORM_FEED = "\u000c";
String text = pageBreakRun.getText();
int breakIndex = text.indexOf(PAGE_BREAK);
if (breakIndex < 0) breakIndex = text.indexOf(FORM_FEED);
// 获取分页符的页码
int pageBreakPageNum = layoutCollector.getStartPageIndex(pageBreakRun);
// 如果分页符前面有内容(同一个Run内),检查是否在同一页
if (breakIndex > 0 && !text.substring(0, breakIndex).trim().isEmpty()) {
// 分页符前面有内容,说明和分页符在同一页,不删除
return 0;
}
// 查找分页符前面的有内容节点
Node prevContentNode = findPrevContentNode(pageBreakRun);
if (prevContentNode == null) {
// 没有前一个内容节点,删除
return pageBreakPageNum;
}
// 获取前一个内容节点的页码
int prevPageNum = layoutCollector.getEndPageIndex(prevContentNode);
// 如果前一个内容节点和分页符不在同一页,删除分页符
if (prevPageNum != pageBreakPageNum) {
return pageBreakPageNum;
}
return 0;
}
/**
* 查找节点前面第一个有内容的节点
*/
@SneakyThrows
private Node findPrevContentNode(Node node) {
if (node.getNodeType() == NodeType.RUN) {
Run run = (Run) node;
Paragraph para = (Paragraph) run.getParentNode();
if (para != null) {
// 在当前段落中找前面的Run
NodeCollection runs = para.getChildNodes(NodeType.RUN, true);
for (int i = 0; i < runs.getCount(); i++) {
Run r = (Run) runs.get(i);
if (r == run) {
// 向前查找有内容的Run
for (int j = i - 1; j >= 0; j--) {
Run prevRun = (Run) runs.get(j);
String prevText = prevRun.getText();
if (prevText != null && !prevText.trim().isEmpty()) {
return prevRun;
}
}
break;
}
}
// 查找当前段落之前的有内容的段落或表格
Node prevSibling = para.getPreviousSibling();
while (prevSibling != null) {
if (prevSibling.getNodeType() == NodeType.PARAGRAPH) {
Paragraph prevPara = (Paragraph) prevSibling;
Node lastRun = getLastContentRun(prevPara);
if (lastRun != null) {
return lastRun;
}
} else if (prevSibling.getNodeType() == NodeType.TABLE) {
return prevSibling;
}
prevSibling = prevSibling.getPreviousSibling();
}
}
} else if (node.getNodeType() == NodeType.PARAGRAPH) {
Paragraph para = (Paragraph) node;
Node prevSibling = para.getPreviousSibling();
while (prevSibling != null) {
if (prevSibling.getNodeType() == NodeType.PARAGRAPH) {
Paragraph prevPara = (Paragraph) prevSibling;
Node lastRun = getLastContentRun(prevPara);
if (lastRun != null) {
return lastRun;
}
} else if (prevSibling.getNodeType() == NodeType.TABLE) {
return prevSibling;
}
prevSibling = prevSibling.getPreviousSibling();
}
} else if (node.getNodeType() == NodeType.TABLE) {
Node prevSibling = node.getPreviousSibling();
while (prevSibling != null) {
if (prevSibling.getNodeType() == NodeType.PARAGRAPH) {
Paragraph prevPara = (Paragraph) prevSibling;
Node lastRun = getLastContentRun(prevPara);
if (lastRun != null) {
return lastRun;
}
} else if (prevSibling.getNodeType() == NodeType.TABLE) {
return prevSibling;
}
prevSibling = prevSibling.getPreviousSibling();
}
}
return null;
}
/**
* 获取段落中最后一个有内容的Run
*/
@SneakyThrows
private Run getLastContentRun(Paragraph para) {
if (!hasRealContent(para)) {
return null;
}
NodeCollection runs = para.getChildNodes(NodeType.RUN, true);
for (int i = runs.getCount() - 1; i >= 0; i--) {
Run r = (Run) runs.get(i);
String rText = r.getText();
if (rText != null && !rText.trim().isEmpty()) {
return r;
}
}
return null;
}
/**
* 检查段落是否有实际内容
*
* @param para 段落
* @return 是否有实际内容
*/
@SneakyThrows
private boolean hasRealContent(Paragraph para) {
String text = para.toString(SaveFormat.TEXT).trim();
// 移除控制字符后检查
String cleanText = text.replace("\f", "").replace("\u000c", "")
.replace("\r", "").replace("\n", "").trim();
return !cleanText.isEmpty();
}
参考:
更多推荐
所有评论(0)