liding
7 天以前 3728fa293e1faf4bf4f9bf41105b84ddb43dc82e
inspect-server/src/main/java/com/ruoyi/inspect/util/ReportSealLayoutUtil.java
@@ -1,6 +1,8 @@
package com.ruoyi.inspect.util;
import com.aspose.words.Document;
import com.aspose.words.Cell;
import com.aspose.words.CompositeNode;
import com.aspose.words.LayoutCollector;
import com.aspose.words.LayoutEnumerator;
import com.aspose.words.License;
@@ -11,6 +13,7 @@
import com.aspose.words.Paragraph;
import com.aspose.words.RelativeHorizontalPosition;
import com.aspose.words.RelativeVerticalPosition;
import com.aspose.words.Run;
import com.aspose.words.SaveFormat;
import com.aspose.words.Section;
import com.aspose.words.Shape;
@@ -75,6 +78,98 @@
        document.save(wordPath, SaveFormat.DOCX);
    }
    /** 从历史批准稿中清除批准内容,恢复重新批准所需的占位符。 */
    public static void restoreForReview(String wordPath) throws Exception {
        applyAsposeLicense();
        Document document = new Document(wordPath);
        List<Shape> shapes = imageShapes(document);
        Shape signatureShape = null;
        List<Shape> dateShapes = new ArrayList<>();
        for (Shape shape : shapes) {
            if (hasPlaceholderMarker(shape, "ratifyUrl")) {
                signatureShape = shape;
                continue;
            }
            if (hasPlaceholderMarker(shape, "ratifyDateUrl")) {
                dateShapes.add(shape);
                continue;
            }
            // 批准时添加的报告章和 MA 章按页面浮动定位,按位置清理而非比较图片字节。
            if (isApprovalSeal(shape)) {
                shape.remove();
            }
        }
        if (signatureShape != null) {
            replaceWithPlaceholder(document, signatureShape, "{{@ratifyUrl}}");
        } else if (!restoreApprovalCellPlaceholder(document)) {
            throw new IllegalStateException("未找到批准栏,无法恢复批准签名占位符");
        }
        // 部分报告模板没有批准日期图片,批准时间仅存在数据库中。
        for (Shape dateShape : dateShapes) {
            replaceWithPlaceholder(document, dateShape, "{{@ratifyDateUrl}}");
        }
        document.save(wordPath, SaveFormat.DOCX);
    }
    private static boolean isApprovalSeal(Shape shape) throws Exception {
        return shape.getWrapType() != WrapType.INLINE
                && shape.getRelativeHorizontalPosition() == RelativeHorizontalPosition.PAGE
                && shape.getRelativeVerticalPosition() == RelativeVerticalPosition.PAGE
                && shape.getWidth() >= 50D && shape.getWidth() <= 140D
                && shape.getHeight() >= 50D && shape.getHeight() <= 140D;
    }
    private static boolean restoreApprovalCellPlaceholder(Document document) throws Exception {
        NodeCollection cells = document.getChildNodes(NodeType.CELL, true);
        for (int i = 0; i < cells.getCount(); i++) {
            Cell label = (Cell) cells.get(i);
            if (!"批准".equals(label.getText().replace("\r", "").replace("\u0007", "").trim())) {
                continue;
            }
            Node sibling = label.getNextSibling();
            if (!(sibling instanceof Cell)) {
                continue;
            }
            Cell signatureCell = (Cell) sibling;
            for (Shape shape : imageShapesIn(signatureCell)) {
                shape.remove();
            }
            Paragraph paragraph = signatureCell.getFirstParagraph();
            if (paragraph == null) {
                throw new IllegalStateException("批准签名单元格缺少段落");
            }
            if (!signatureCell.getText().contains("{{@ratifyUrl}}")) {
                paragraph.appendChild(new Run(document, "{{@ratifyUrl}}"));
            }
            return true;
        }
        return false;
    }
    private static List<Shape> imageShapesIn(Cell cell) throws Exception {
        List<Shape> result = new ArrayList<>();
        NodeCollection nodes = cell.getChildNodes(NodeType.SHAPE, true);
        for (int i = 0; i < nodes.getCount(); i++) {
            Shape shape = (Shape) nodes.get(i);
            if (shape.hasImage()) {
                result.add(shape);
            }
        }
        return result;
    }
    private static boolean hasPlaceholderMarker(Shape shape, String marker) throws Exception {
        String alternativeText = shape.getAlternativeText();
        return alternativeText != null && alternativeText.contains(marker);
    }
    @SuppressWarnings({"rawtypes", "unchecked"})
    private static void replaceWithPlaceholder(Document document, Shape shape, String placeholder) {
        CompositeNode parent = (CompositeNode) shape.getParentNode();
        parent.insertBefore(new Run(document, placeholder), shape);
        shape.remove();
    }
    private static void applyAsposeLicense() throws Exception {
        try (InputStream inputStream = ReportSealLayoutUtil.class.getResourceAsStream("/lib/license.xml")) {
            if (inputStream == null) {
@@ -93,7 +188,7 @@
            return null;
        }
        Path path = java.nio.file.Paths.get(imagePath);
        return Files.isRegularFile(path) ? Files.readAllBytes(path) : null;
        return Files.readAllBytes(path);
    }
    private static void removeMatchingImages(Document document, byte[] imageBytes) throws Exception {
@@ -115,12 +210,18 @@
    private static void addCoverReportSeal(Document document, String imagePath) throws Exception {
        Paragraph anchor = firstBodyParagraph(document);
        LayoutCollector collector = new LayoutCollector(document);
        Paragraph company = findPageCompanyParagraph(document, collector, 1);
        Rectangle2D rectangle = company == null ? null : getLayoutRectangle(document, collector, company);
        PageSetup pageSetup = document.getFirstSection().getPageSetup();
        double[] size = fitImage(imagePath, REPORT_MAX_WIDTH, REPORT_MAX_HEIGHT);
        Shape shape = createFloatingImage(document, imagePath, size[0], size[1]);
        shape.setLeft(pageSetup.getLeftMargin() + 20D);
        shape.setTop(pageSetup.getPageHeight() - pageSetup.getBottomMargin()
                - size[1] - POSITION_GAP);
        shape.setLeft((pageSetup.getPageWidth() - size[0]) / 2D);
        // 封面章与底部机构名称重叠,随名称的实际排版位置移动。
        double preferredTop = rectangle == null
                ? pageSetup.getPageHeight() - pageSetup.getBottomMargin() - size[1] - POSITION_GAP
                : rectangle.getCenterY() - size[1] / 2D;
        shape.setTop(clamp(preferredTop, 0D, pageSetup.getPageHeight() - size[1]));
        shape.setBehindText(true);
        anchor.appendChild(shape);
    }
@@ -157,7 +258,9 @@
    private static void addLastPageReportSeal(Document document, String imagePath) throws Exception {
        int lastPage = document.getPageCount();
        LayoutCollector collector = new LayoutCollector(document);
        Paragraph anchor = findLastPageAnchor(document, collector, lastPage);
        Paragraph title = findResultTitle(document);
        // 结果表可能跨页,章应跟随结果标题,不能跟随最后一页的“报告结束”。
        Paragraph anchor = title == null ? findLastPageAnchor(document, collector, lastPage) : title;
        if (anchor == null) {
            return;
        }
@@ -167,11 +270,23 @@
        Shape shape = createFloatingImage(document, imagePath, size[0], size[1]);
        shape.setLeft((pageSetup.getPageWidth() - size[0]) / 2D);
        Rectangle2D rectangle = getLayoutRectangle(document, collector, anchor);
        double preferredTop = rectangle == null
                ? pageSetup.getPageHeight() - pageSetup.getBottomMargin() - size[1] - POSITION_GAP
                : rectangle.getY() + rectangle.getHeight() + POSITION_GAP;
        double preferredTop;
        if (title != null) {
            Paragraph company = findPageCompanyParagraph(document, collector, collector.getStartPageIndex(title));
            Rectangle2D companyRectangle = company == null ? null : getLayoutRectangle(document, collector, company);
            // 章的中心对准机构名称与结果标题组成的区域,允许伸入上页边距。
            double headingTop = companyRectangle == null || rectangle == null
                    ? (rectangle == null ? pageSetup.getTopMargin() : rectangle.getY())
                    : Math.min(companyRectangle.getY(), rectangle.getY());
            double headingBottom = rectangle == null ? headingTop : rectangle.getMaxY();
            preferredTop = (headingTop + headingBottom - size[1]) / 2D;
        } else {
            preferredTop = rectangle == null
                    ? pageSetup.getPageHeight() - pageSetup.getBottomMargin() - size[1] - POSITION_GAP
                    : rectangle.getY() + rectangle.getHeight() + POSITION_GAP;
        }
        shape.setTop(clamp(preferredTop,
                pageSetup.getTopMargin(),
                title == null ? pageSetup.getTopMargin() : 0D,
                pageSetup.getPageHeight() - pageSetup.getBottomMargin() - size[1]));
        shape.setBehindText(true);
        anchor.appendChild(shape);
@@ -180,7 +295,14 @@
    private static Shape createFloatingImage(Document document, String imagePath,
                                              double width, double height) throws Exception {
        Shape shape = new Shape(document, ShapeType.IMAGE);
        shape.getImageData().setImage(imagePath);
        // Aspose 15 会把 Linux 的 / 开头路径当作相对路径,可能生成空图片且不报错。
        // 通过流嵌入真实图片内容,避免路径被按文档的资源地址再次解析。
        try (InputStream imageStream = Files.newInputStream(java.nio.file.Paths.get(imagePath))) {
            shape.getImageData().setImage(imageStream);
        }
        if (!shape.hasImage()) {
            throw new IllegalStateException("报告印章图片嵌入失败");
        }
        shape.setWidth(width);
        shape.setHeight(height);
        shape.setWrapType(WrapType.NONE);
@@ -220,6 +342,42 @@
        return fallback;
    }
    private static Paragraph findPageCompanyParagraph(Document document, LayoutCollector collector, int page)
            throws Exception {
        Paragraph company = null;
        NodeCollection paragraphs = document.getChildNodes(NodeType.PARAGRAPH, true);
        for (int i = 0; i < paragraphs.getCount(); i++) {
            Paragraph paragraph = (Paragraph) paragraphs.get(i);
            if (paragraph.getAncestor(NodeType.BODY) != null
                    && paragraph.getAncestor(NodeType.CELL) == null
                    && collector.getStartPageIndex(paragraph) == page
                    && normalizedText(paragraph).endsWith("公司")) {
                company = paragraph;
            }
        }
        return company;
    }
    private static Paragraph findResultTitle(Document document) throws Exception {
        NodeCollection paragraphs = document.getChildNodes(NodeType.PARAGRAPH, true);
        for (int i = 0; i < paragraphs.getCount(); i++) {
            Paragraph paragraph = (Paragraph) paragraphs.get(i);
            if (paragraph.getAncestor(NodeType.BODY) == null
                    || paragraph.getAncestor(NodeType.CELL) != null) {
                continue;
            }
            String text = normalizedText(paragraph);
            if ("检验结果".equals(text) || "检测结果".equals(text)) {
                return paragraph;
            }
        }
        return null;
    }
    private static String normalizedText(Paragraph paragraph) {
        return paragraph.getText().replaceAll("[\\s\\u3000]+", "");
    }
    private static Paragraph findBodyParagraphContaining(Document document, String text,
                                                          LayoutCollector collector, Integer page)
            throws Exception {