| | |
| | | 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; |
| | |
| | | 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; |
| | |
| | | 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) { |
| | |
| | | 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 { |
| | |
| | | |
| | | 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); |
| | | } |
| | |
| | | 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; |
| | | } |
| | |
| | | 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); |
| | |
| | | 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); |
| | |
| | | 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 { |