我使用的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();
    }

参考:

表格跨页导致页码不对

wps和office渲染存在差别

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐