使用 AsposeWord 向 word 中的文字添加标签

背景

最近做套打服务,产品想感知到点击模板中系统插入的占位符时,需要在前端能够弹出属性设置页面。 在线编辑服务使用的是 onlyoffice, 此文不讨论 onlyoffice 如何感知到点击哪个标签的。仅讨论功能完成后如何将历史的模板升级到带标签的模板。

相关工具

在线编辑服务:onlyoffice 社区版 ------免费开源 文档处理工具:Aspose Word For Java ------ 需要购买,也有破解版。

单元测试

服务中插入的占位符是 ${xxx.xxx} 格式的,所以测试用例中根据此进行书签的添加。有需要的话可以根据自身条件编写不同的日志。

java 复制代码
    @Test
    public void testAddBookMark() {
        try (InputStream resourceAsStream = DocumentTest.class.getClassLoader().getResourceAsStream("历史模板.docx")) {
            Document doc = new Document(resourceAsStream);
            // 需要添加书签的目标文本列表
            Map<String, String> bookmarkData = new LinkedHashMap<>();
            processDocument(doc, bookmarkData);

            bookmarkData.forEach((k, v) -> System.out.println(k + " -> " + v));
            // 保存文档
            doc.save("E:\\project\\java\\ggfw-upgrade-tool-op\\upgrade-service\\src\\test\\resources\\历史模板升级后.docx");

            for (Map.Entry<String, String> entry : bookmarkData.entrySet()) {
                System.out.println("UUID: " + entry.getKey() + ", 标签文本: " + entry.getValue());
            }
        } catch (Exception e) {
            log.error("error:", e);
        }
    }

    private static final Pattern TAG_PATTERN = Pattern.compile("\\$\\{.*?}");

    private static void processDocument(Document doc, Map<String, String> bookmarkData) throws Exception {
        List<RunInfo> allRuns = collectRunInfo(doc);
        String fullText = buildFullText(allRuns);

        Matcher matcher = TAG_PATTERN.matcher(fullText);
        while (matcher.find()) {
            String target = matcher.group();
            int globalStart = matcher.start();
            int globalEnd = matcher.end();

            List<RunSegment> segments = locateSegments(allRuns, globalStart, globalEnd);
            if (!segments.isEmpty()) {
                String bookmarkName = UUID.randomUUID().toString();
                applyBookmarks(doc, segments, bookmarkName);
                bookmarkData.put(bookmarkName, target);
            }
        }
    }

    // 收集所有Run信息
    private static List<RunInfo> collectRunInfo(Document doc) throws Exception {
        List<RunInfo> runs = new ArrayList<>();
        int currentPosition = 0;

        for (Paragraph para : (Iterable<Paragraph>) doc.getChildNodes(NodeType.PARAGRAPH, true)) {
            for (Run run : (Iterable<Run>) para.getChildNodes(NodeType.RUN, true)) {
                String text = run.getText();
                runs.add(new RunInfo(run, currentPosition, text.length()));
                currentPosition += text.length();
            }
        }
        return runs;
    }

    // 构建完整文本
    private static String buildFullText(List<RunInfo> runs) {
        StringBuilder sb = new StringBuilder();
        for (RunInfo ri : runs) {
            sb.append(ri.run.getText());
        }
        return sb.toString();
    }

    // 定位目标文本所在的Run段
    private static List<RunSegment> locateSegments(List<RunInfo> allRuns, int globalStart, int globalEnd) {
        List<RunSegment> segments = new ArrayList<>();
        int remainingLength = globalEnd - globalStart;
        int currentGlobal = globalStart;

        for (RunInfo ri : allRuns) {
            int runStart = ri.globalStart;
            int runEnd = ri.globalStart + ri.length;

            if (runEnd <= currentGlobal) continue;
            if (runStart >= globalEnd) break;

            int localStart = Math.max(currentGlobal - runStart, 0);
            int localEnd = Math.min(localStart + remainingLength, ri.length);

            segments.add(new RunSegment(ri.run, localStart, localEnd));

            remainingLength -= (localEnd - localStart);
            currentGlobal += (localEnd - localStart);

            if (remainingLength <= 0) break;
        }
        return segments;
    }

    // 应用书签到文档
    private static void applyBookmarks(Document doc, List<RunSegment> segments, String bookmarkName) throws Exception {
        List<Node> newNodes = new ArrayList<>();
        Run firstRun = segments.get(0).run;
        CompositeNode parent = firstRun.getParentNode();

        // 处理第一个Run
        RunSegment firstSeg = segments.get(0);
        splitRun(firstSeg.run, firstSeg.start, firstSeg.end, newNodes, true);

        // 处理中间Run
        for (int i = 1; i < segments.size() - 1; i++) {
            RunSegment seg = segments.get(i);
            splitRun(seg.run, seg.start, seg.end, newNodes, false);
        }

        // 处理最后一个Run
        if (segments.size() > 1) {
            RunSegment lastSeg = segments.get(segments.size() - 1);
            splitRun(lastSeg.run, lastSeg.start, lastSeg.end, newNodes, false);
        }

        // 插入书签
        BookmarkStart start = new BookmarkStart(doc, bookmarkName);
        BookmarkEnd end = new BookmarkEnd(doc, bookmarkName);

        Node targetStart = newNodes.get(1); // 第一个目标段
        Node targetEnd = newNodes.get(newNodes.size() - 1); // 最后一个目标段

        parent.insertBefore(start, targetStart);
        parent.insertAfter(end, targetEnd);
    }

    // 分割Run并保留格式
    private static void splitRun(Run original, int start, int end, List<Node> newNodes, boolean isFirst) {
        String text = original.getText();
        CompositeNode parent = original.getParentNode();

        // 创建前段
        if (isFirst && start > 0) {
            Run before = (Run) original.deepClone(true);
            before.setText(text.substring(0, start));
            newNodes.add(before);
        }

        // 创建目标段
        Run target = (Run) original.deepClone(true);
        target.setText(text.substring(start, end));
        newNodes.add(target);

        // 创建后段
        if (end < text.length()) {
            Run after = (Run) original.deepClone(true);
            after.setText(text.substring(end));
            newNodes.add(after);
        }

        // 替换原始节点
        for (Node node : newNodes) {
            parent.insertBefore(node, original);
        }
        parent.removeChild(original);
    }

    // Helper classes
    private static class RunInfo {
        Run run;
        int globalStart;
        int length;

        RunInfo(Run run, int globalStart, int length) {
            this.run = run;
            this.globalStart = globalStart;
            this.length = length;
        }
    }

    private static class RunSegment {
        Run run;
        int start;
        int end;

        RunSegment(Run run, int start, int end) {
            this.run = run;
            this.start = start;
            this.end = end;
        }
    }
相关推荐
老任与码24 分钟前
Spring AI Alibaba(1)——基本使用
java·人工智能·后端·springaialibaba
华子w9089258591 小时前
基于 SpringBoot+VueJS 的农产品研究报告管理系统设计与实现
vue.js·spring boot·后端
星辰离彬1 小时前
Java 与 MySQL 性能优化:Java应用中MySQL慢SQL诊断与优化实战
java·后端·sql·mysql·性能优化
GetcharZp3 小时前
彻底告别数据焦虑!这款开源神器 RustDesk,让你自建一个比向日葵、ToDesk 更安全的远程桌面
后端·rust
jack_yin4 小时前
Telegram DeepSeek Bot 管理平台 发布啦!
后端
小码编匠4 小时前
C# 上位机开发怎么学?给自动化工程师的建议
后端·c#·.net
库森学长4 小时前
面试官:发生OOM后,JVM还能运行吗?
jvm·后端·面试
转转技术团队4 小时前
二奢仓店的静默打印代理实现
java·后端
蓝易云4 小时前
CentOS 7上安装X virtual framebuffer (Xvfb) 的步骤以及如何解决无X服务器的问题
前端·后端·centos
秋千码途5 小时前
小架构step系列07:查找日志配置文件
spring boot·后端·架构