如何获取word中header和footer段落的坐标(x, y)?

您好:我使用aspose-words-21.5.0-jdk17.jar版本生成word。生成的word包含header和footer,还有body。分别是三个table,每个table的的cell都设置了段落。然后把header、body和footer中某一个段落放入到集合中,遍历集合move到对应段落,然后插入对应段落的shape。以下代码注释了计算坐标的代码就可以正常运行。

但是现在有这样的问题:我需要在插入shape的时候重新计算shape的大小和相对位置,因为有可能会超过当前的word页面,所以我需要计算需要插入shape的段落的坐标(x,y)。然后去计算新的相对位置。出入body中的段落计算没有问题,但是传入header和footer中的段落,计算的时候就报错了。不清楚师什么原因。请帮忙指出错误原因?并且提供计算header和footer中段落的坐标(x, y)。感谢。

如有问题随时给我留言,感谢。

package com.edetek.conform.solution.util;

import java.awt.Color;
import java.awt.geom.Rectangle2D;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

import com.aspose.words.Document;
import com.aspose.words.DocumentBuilder;
import com.aspose.words.HeaderFooterType;
import com.aspose.words.LayoutCollector;
import com.aspose.words.LayoutEnumerator;
import com.aspose.words.License;
import com.aspose.words.Node;
import com.aspose.words.NodeType;
import com.aspose.words.Paragraph;
import com.aspose.words.RelativeHorizontalPosition;
import com.aspose.words.RelativeVerticalPosition;
import com.aspose.words.Run;
import com.aspose.words.Shape;
import com.aspose.words.ShapeType;
import com.edetek.conform.common.exception.ToastMessageException;

/**
 * @author DongZhou
 * @since 2025/8/11 15:29
 */
public class DD {

    private static final String LICENSE_RESOURCE_NAME = "license.xml";
    protected static final License ASPOSE_LICENSE = new License();

    static {
        try (InputStream is = InitAsposeLicense.class.getClassLoader().getResourceAsStream(LICENSE_RESOURCE_NAME)) {
            ASPOSE_LICENSE.setLicense(is);
        } catch (Exception e) {
            throw new ToastMessageException("The aspose license is not found");
        }
    }

    public static class ParaPosition {
        public int pageIndex;
        public double x;
        public double y;
        public double width;
        public double height;

        @Override
        public String toString() {
            return String.format("Page=%d, X=%.2fpt, Y=%.2fpt, W=%.2fpt, H=%.2fpt",
                    pageIndex, x, y, width, height);
        }
    }


    public static void main(String[] args) throws Exception {
        // 创建文档和文档构建器
        Document doc = new Document();
        DocumentBuilder builder = new DocumentBuilder(doc);

        List<Paragraph> paragraphs = new ArrayList<>();
        builder.moveToHeaderFooter(HeaderFooterType.HEADER_PRIMARY);
        // 插入一个表格
        builder.startTable();

        // ---- 表头: 2行3列 ----
        for (int i = 0; i < 2; i++) {
            for (int j = 0; j < 3; j++) {
                builder.insertCell();
                builder.write(String.format("Header R%dC%d", i + 1, j + 1));
                if (i == 1 && j == 2) {
                    // 在表头第一行第一列插入一个段落
                    Paragraph headerPara = builder.getCurrentParagraph();
                    Run run = new Run(doc, "Header Paragraph");
                    headerPara.appendChild(run);
                    paragraphs.add(headerPara); // 保存段落引用
                }
            }
            builder.endRow();
        }
        builder.endTable();

        builder.moveToDocumentStart(); // 或 moveToDocumentEnd()
        builder.startTable();
        // ---- 正文: 5行5列 ----
        for (int i = 0; i < 5; i++) {
            for (int j = 0; j < 5; j++) {
                builder.insertCell();
                builder.write(String.format("Body R%dC%d", i + 1, j + 1));
                if (i == 2 && j == 3) {
                    // 在正文第三行第四列插入一个段落
                    Paragraph bodyPara = builder.getCurrentParagraph();
                    Run run = new Run(doc, "Body Paragraph");
                    bodyPara.appendChild(run);
                    paragraphs.add(bodyPara); // 保存段落引用
                }
            }
            builder.endRow();
        }
        builder.endTable();

        builder.moveToHeaderFooter(HeaderFooterType.FOOTER_PRIMARY);
        builder.startTable();
        // ---- 表尾: 2行3列 ----
        for (int i = 0; i < 2; i++) {
            for (int j = 0; j < 3; j++) {
                builder.insertCell();
                builder.write(String.format("Footer R%dC%d", i + 1, j + 1));
                if (i == 1 && j == 0) {
                    // 在表尾第二行第一列插入一个段落
                    Paragraph footerPara = builder.getCurrentParagraph();
                    Run run = new Run(doc, "Footer Paragraph");
                    footerPara.appendChild(run);
                    paragraphs.add(footerPara); // 保存段落引用
                }
            }
            builder.endRow();
        }
        builder.endTable();

        for (Paragraph para : paragraphs) {
            // 计算para的坐标
            ParaPosition paragraphPagePosition = getParagraphPagePosition(para, doc);
            System.out.println("Paragraph Position: " + paragraphPagePosition);
            // 在每个段落后插入一个Shape
            insertShapeAtParagraph(builder, para, 50, 20, 100, 20, Color.YELLOW, "Shape Text");
        }

        String basePath = "D:\\temp\\path\\";
        // 保存文档
        doc.save(basePath + "TableWithShapes.docx");
    }

    public static DD.ParaPosition getParagraphPagePosition(Paragraph paragraph, Document doc) throws Exception {
        if (paragraph == null || doc == null) {
            throw new IllegalArgumentException("Parameters 'paragraph' and 'doc' is null.");
        }

        doc.updatePageLayout();
        // Initialize LayoutCollector and update page layout to ensure layout info is up-to-date
        LayoutCollector collector = new LayoutCollector(doc);

        return getBodyParagraphPosition(paragraph, collector);
    }

    private static DD.ParaPosition getBodyParagraphPosition(Paragraph paragraph, LayoutCollector collector) throws Exception {
        LayoutEnumerator enumerator = new LayoutEnumerator(collector.getDocument());

        // Try to get any layout entity related to the paragraph
        Object layoutEntity = getAnyLayoutEntityFromParagraphChildren(paragraph, collector);
        if (layoutEntity == null) {
            throw new IllegalStateException("Unable to obtain layout entity for body paragraph.");
        }
        enumerator.setCurrent(layoutEntity);

        // If the paragraph is inside a table, move up to the Cell level to get correct coordinates
        Node ancestorCell = paragraph.getAncestor(com.aspose.words.Cell.class);
        if (ancestorCell != null) {
            Object cellEntity = collector.getEntity(ancestorCell);
            if (cellEntity != null) {
                enumerator.setCurrent(cellEntity);
            }
        } else {
            // Otherwise, move up to the paragraph level
            while (enumerator.getType() != NodeType.PARAGRAPH && enumerator.moveParent()) {
                // empty loop
            }
        }

        Rectangle2D.Float rect = enumerator.getRectangle();
        if (rect == null) {
            throw new IllegalStateException("Unable to get bounding rectangle for body paragraph.");
        }

        DD.ParaPosition pos = new DD.ParaPosition();
        pos.pageIndex = collector.getStartPageIndex(paragraph);
        pos.x = rect.getX();
        pos.y = rect.getY();
        pos.width = rect.getWidth();
        pos.height = rect.getHeight();
        return pos;
    }

    private static Object getAnyLayoutEntityFromParagraphChildren(Paragraph paragraph,
                                                                  LayoutCollector collector) throws Exception {
        for (Node child = paragraph.getFirstChild(); child != null; child = child.getNextSibling()) {
            Object entity = collector.getEntity(child);
            if (entity != null) return entity;
        }
        for (Run run : paragraph.getRuns()) {
            Object entity = collector.getEntity(run);
            if (entity != null) return entity;
        }
        return collector.getEntity(paragraph);
    }

    /**
     * 使用DocumentBuilder移动到段落,插入Shape,设置相对位置
     *
     * @param builder DocumentBuilder对象
     * @param para 目标段落
     * @param offsetX 相对段落的X偏移(磅)
     * @param offsetY 相对段落的Y偏移(磅)
     * @param width Shape宽度
     * @param height Shape高度
     * @param color Shape颜色
     * @param text Shape内显示文字
     */
    private static void insertShapeAtParagraph(DocumentBuilder builder, Paragraph para,
                                               double offsetX, double offsetY,
                                               double width, double height,
                                               Color color, String text) throws Exception {
        builder.moveTo(para);

        // 插入shape,设置相对段落偏移
        Shape shape = new Shape(builder.getDocument(), ShapeType.RECTANGLE);
        shape.setWidth(width);
        shape.setHeight(height);
        shape.setRelativeHorizontalPosition(RelativeHorizontalPosition.COLUMN);
        shape.setRelativeVerticalPosition(RelativeVerticalPosition.PARAGRAPH);
        shape.setLeft(offsetX);
        shape.setTop(offsetY);
        shape.getFill().setColor(color);
        shape.getStroke().setColor(Color.BLACK);

        // 插入形状内文本
        Paragraph shapePara = new Paragraph(builder.getDocument());
        Run run = new Run(builder.getDocument(), text);
        shapePara.appendChild(run);
        shape.appendChild(shapePara);

        builder.insertNode(shape);
    }

}

@imzdong

要获取Word文档中页眉和页脚段落的坐标(x, y),您可以使用Aspose.Words的LayoutCollectorLayoutEnumerator类。以下是一个示例代码,展示如何计算页眉和页脚中段落的坐标:

public static ParaPosition getHeaderFooterParagraphPosition(Paragraph paragraph, Document doc) throws Exception {
    if (paragraph == null || doc == null) {
        throw new IllegalArgumentException("Parameters 'paragraph' and 'doc' cannot be null.");
    }

    doc.updatePageLayout();
    LayoutCollector collector = new LayoutCollector(doc);
    LayoutEnumerator enumerator = new LayoutEnumerator(doc);

    // 获取段落的布局实体
    Object layoutEntity = collector.getEntity(paragraph);
    if (layoutEntity == null) {
        throw new IllegalStateException("Unable to obtain layout entity for the paragraph.");
    }
    enumerator.setCurrent(layoutEntity);

    // 获取段落的坐标
    Rectangle2D.Float rect = enumerator.getRectangle();
    if (rect == null) {
        throw new IllegalStateException("Unable to get bounding rectangle for the paragraph.");
    }

    ParaPosition pos = new ParaPosition();
    pos.pageIndex = collector.getStartPageIndex(paragraph);
    pos.x = rect.getX();
    pos.y = rect.getY();
    pos.width = rect.getWidth();
    pos.height = rect.getHeight();
    return pos;
}

在这个示例中,您可以调用getHeaderFooterParagraphPosition方法,传入页眉或页脚中的段落和文档对象,以获取该段落的坐标信息。确保在调用此方法之前,文档的页面布局已更新。

如果在计算页眉或页脚段落的坐标时遇到错误,可能是因为该段落没有有效的布局实体。请确保段落确实存在于文档中,并且在调用计算坐标的方法之前,文档的布局已更新。

请按照上述步骤操作,以确保您能够成功获取坐标信息。如果您有其他问题或需要进一步的帮助,请随时告诉我!

你这段代码报错呀,总是为null

Object layoutEntity = collector.getEntity(paragraph);
if (layoutEntity == null) {
throw new IllegalStateException(“Unable to obtain layout entity for the paragraph.”);
}

@imzdong 遗憾的是,它不适用于 Run、Cell、Row 或 Table 节点以及页眉/页脚中的节点。页眉/页脚的问题在于,页眉/页脚中的单个文档节点在布局模型中有多个对应对象–每个渲染该页眉/页脚的页面都有一个对应对象。因此,从技术上讲,不可能为页眉/页脚中的节点返回单一实体。

您可以使用以下代码在文档中遍历节点,找到与页眉/页脚内容相关的节点:

Document doc = new Document("input.docx");

// Create an enumerator that can traverse these entities like a tree.
LayoutEnumerator layoutEnumerator = new LayoutEnumerator(doc);

System.out.println("Traversing from first to last, elements between pages separated:");
traverseLayoutForward(layoutEnumerator, 1);


/// <summary>
/// Enumerate through layoutEnumerator's layout entity collection front-to-back,
/// in a depth-first manner, and in the "Visual" order.
/// </summary>
private static void traverseLayoutForward(LayoutEnumerator layoutEnumerator, int depth) throws Exception {
    do {
        printCurrentEntity(layoutEnumerator, depth);

        if (layoutEnumerator.moveFirstChild()) {
            traverseLayoutForward(layoutEnumerator, depth + 1);
            layoutEnumerator.moveParent();
        }
    } while (layoutEnumerator.moveNext());
}

/// <summary>
/// Print information about layoutEnumerator's current entity to the console, while indenting the text with tab characters
/// based on its depth relative to the root node that we provided in the constructor LayoutEnumerator instance.
/// The rectangle that we process at the end represents the area and location that the entity takes up in the document.
/// </summary>
private static void printCurrentEntity(LayoutEnumerator layoutEnumerator, int indent) throws Exception {
    String tabs = StringUtils.repeat("\t", indent);

    System.out.println(layoutEnumerator.getKind().equals("")
            ? MessageFormat.format("{0}-> Entity type: {1}", tabs, layoutEnumerator.getType())
            : MessageFormat.format("{0}-> Entity type & kind: {1}, {2}", tabs, layoutEnumerator.getType(), layoutEnumerator.getKind()));

    // Only spans can contain text.
    if (layoutEnumerator.getType() == LayoutEntityType.SPAN)
        System.out.println(MessageFormat.format("{0}   Span contents: \"{1}\"", tabs, layoutEnumerator.getText()));

    Rectangle2D.Float leRect = layoutEnumerator.getRectangle();
    System.out.println(MessageFormat.format("{0}   Rectangle dimensions {1}x{2}, X={3} Y={4}", tabs, leRect.getWidth(), leRect.getHeight(), leRect.getX(), leRect.getY()));
    System.out.println(MessageFormat.format("{0}   Page {1}", tabs, layoutEnumerator.getPageIndex()));
}

可能的输出结果如下(举例说明):

-> Entity type & kind: 1,024, FIRSTPAGEHEADER
	Rectangle dimensions 467.75x20.954, X=85.05 Y=35.4
	Page 1
	-> Entity type: 32
	   Rectangle dimensions 467.75x20.954, X=85.05 Y=35.4
	   Page 1
		-> Entity type & kind: 64, SHAPE
		   Span contents: "null"
		   Rectangle dimensions 33x20.954, X=85.05 Y=35.4
		   Page 1
		-> Entity type & kind: 64, TEXT
		   Span contents: "First"
		   Rectangle dimensions 19.4x20.954, X=118.05 Y=35.4
		   Page 1
		-> Entity type & kind: 64, SPACES
		   Span contents: " "
		   Rectangle dimensions 2.487x20.954, X=137.45 Y=35.4
		   Page 1
		-> Entity type & kind: 64, TEXT
		   Span contents: "header"
		   Rectangle dimensions 31.609x20.954, X=139.937 Y=35.4
		   Page 1
		-> Entity type & kind: 64, PARAGRAPH
		   Span contents: "¶"
		   Rectangle dimensions 6.445x20.954, X=171.546 Y=35.4
		   Page 1