Showing
5 changed files
with
301 additions
and
9 deletions
| ... | ... | @@ -1194,7 +1194,7 @@ public class ContractDistributorStandardController extends DefaultBaseController |
| 1194 | 1194 | addFormulaComponent(formulaComponentList, line.getWidth(), line.getWidthTolPos(), line.getWidthTolNeg()); |
| 1195 | 1195 | addFormulaComponent(formulaComponentList, line.getLength(), line.getLengthTolPos(), line.getLengthTolNeg()); |
| 1196 | 1196 | |
| 1197 | - String latex = LatexFormulaExcelExporterUtil.convertToContractSpecLatexSingleLineFixed3(formulaComponentList); | |
| 1197 | + String latex = LatexFormulaExcelExporterUtil.convertToContractSpecLatexSingleLineFixed3Compact(formulaComponentList); | |
| 1198 | 1198 | if (StringUtils.isNotBlank(latex)) { |
| 1199 | 1199 | specImageCells.add(new SpecImageCell(startRow, 5, latex)); |
| 1200 | 1200 | } |
| ... | ... | @@ -1238,7 +1238,7 @@ public class ContractDistributorStandardController extends DefaultBaseController |
| 1238 | 1238 | addFormulaComponent(formulaComponentList, line.getWidth(), line.getWidthTolPos(), line.getWidthTolNeg()); |
| 1239 | 1239 | addFormulaComponent(formulaComponentList, line.getLength(), line.getLengthTolPos(), line.getLengthTolNeg()); |
| 1240 | 1240 | |
| 1241 | - String latex = LatexFormulaExcelExporterUtil.convertToContractSpecLatexSingleLineFixed3(formulaComponentList); | |
| 1241 | + String latex = LatexFormulaExcelExporterUtil.convertToContractSpecLatexSingleLineFixed3Compact(formulaComponentList); | |
| 1242 | 1242 | if (StringUtils.isNotBlank(latex)) { |
| 1243 | 1243 | specImageCells.add(new SpecImageCell(startRow, 9, latex)); |
| 1244 | 1244 | } |
| ... | ... | @@ -1253,13 +1253,14 @@ public class ContractDistributorStandardController extends DefaultBaseController |
| 1253 | 1253 | } |
| 1254 | 1254 | } |
| 1255 | 1255 | |
| 1256 | - double specMinScale = 1.0; | |
| 1257 | 1256 | for (SpecImageCell specCell : specImageCells) { |
| 1258 | - double scale = LatexFormulaExcelExporterUtil.calculateFitScale(workbook, sheet, specCell.latex, specCell.rowIndex, specCell.colIndex); | |
| 1259 | - specMinScale = Math.min(specMinScale, scale); | |
| 1260 | - } | |
| 1261 | - for (SpecImageCell specCell : specImageCells) { | |
| 1262 | - LatexFormulaExcelExporterUtil.insertLatexImageToCellWithMaxScale(workbook, sheet, specCell.latex, specCell.rowIndex, specCell.colIndex, specMinScale); | |
| 1257 | + LatexFormulaExcelExporterUtil.insertContractLatexImageToCellFillTemplate( | |
| 1258 | + workbook, | |
| 1259 | + sheet, | |
| 1260 | + specCell.latex, | |
| 1261 | + specCell.rowIndex, | |
| 1262 | + specCell.colIndex | |
| 1263 | + ); | |
| 1263 | 1264 | } |
| 1264 | 1265 | |
| 1265 | 1266 | // --- 填充全局变量 --- | ... | ... |
| ... | ... | @@ -109,7 +109,43 @@ public class LatexFormulaExcelExporterUtil { |
| 109 | 109 | return "\\mathbf{\\mathrm{" + actual + "}}"; |
| 110 | 110 | } |
| 111 | 111 | |
| 112 | + public static String convertToContractSpecLatexSingleLineFixed3Compact(List<FormulaComponent> componentList) { | |
| 113 | + if (CollectionUtils.isEmpty(componentList)) { | |
| 114 | + return ""; | |
| 115 | + } | |
| 116 | + | |
| 117 | + int nonNullCount = 0; | |
| 118 | + for (FormulaComponent comp : componentList) { | |
| 119 | + if (comp != null && comp.getBase() != null) { | |
| 120 | + nonNullCount++; | |
| 121 | + } | |
| 122 | + } | |
| 123 | + if (nonNullCount <= 0) { | |
| 124 | + return ""; | |
| 125 | + } | |
| 126 | + | |
| 127 | + String actual = buildLatexLine(componentList, 0, componentList.size(), "\\!\\times\\!"); | |
| 128 | + if (actual.isEmpty()) { | |
| 129 | + return ""; | |
| 130 | + } | |
| 131 | + | |
| 132 | + int missing = Math.max(0, 3 - nonNullCount); | |
| 133 | + if (missing > 0) { | |
| 134 | + StringBuilder phantom = new StringBuilder(); | |
| 135 | + for (int i = 0; i < missing; i++) { | |
| 136 | + phantom.append("\\!\\times\\!000^{+00}_{+00}"); | |
| 137 | + } | |
| 138 | + actual = actual + "\\phantom{" + phantom + "}"; | |
| 139 | + } | |
| 140 | + | |
| 141 | + return "\\mathbf{\\mathrm{" + actual + "}}"; | |
| 142 | + } | |
| 143 | + | |
| 112 | 144 | private static String buildLatexLine(List<FormulaComponent> componentList, int startIndex, int endIndexExclusive) { |
| 145 | + return buildLatexLine(componentList, startIndex, endIndexExclusive, " \\times "); | |
| 146 | + } | |
| 147 | + | |
| 148 | + private static String buildLatexLine(List<FormulaComponent> componentList, int startIndex, int endIndexExclusive, String separator) { | |
| 113 | 149 | StringBuilder latex = new StringBuilder(); |
| 114 | 150 | boolean first = true; |
| 115 | 151 | int end = Math.min(componentList.size(), endIndexExclusive); |
| ... | ... | @@ -119,7 +155,7 @@ public class LatexFormulaExcelExporterUtil { |
| 119 | 155 | continue; |
| 120 | 156 | } |
| 121 | 157 | if (!first) { |
| 122 | - latex.append(" \\times "); | |
| 158 | + latex.append(separator); | |
| 123 | 159 | } |
| 124 | 160 | first = false; |
| 125 | 161 | |
| ... | ... | @@ -253,6 +289,29 @@ public class LatexFormulaExcelExporterUtil { |
| 253 | 289 | insertLatexImageToRegion(workbook, sheet, latex, rowIndex, colIndex, rowRange, colRange, 16, null); |
| 254 | 290 | } |
| 255 | 291 | |
| 292 | + /** | |
| 293 | + * 合同规格专用:按照模板单元格区域高度优先铺满,并根据区域大小动态选择更合适的渲染字号。 | |
| 294 | + */ | |
| 295 | + public static void insertContractLatexImageToCellFillTemplate(Workbook workbook, Sheet sheet, String latex, | |
| 296 | + int rowIndex, int colIndex) throws IOException { | |
| 297 | + int baseFontSize = Math.max(16, resolveCellFontSize(workbook, sheet, rowIndex, colIndex)); | |
| 298 | + CellRangeAddress mergedRegion = findMergedRegion(sheet, rowIndex, colIndex); | |
| 299 | + if (mergedRegion != null) { | |
| 300 | + insertContractLatexImageToRegionFillTemplate( | |
| 301 | + workbook, | |
| 302 | + sheet, | |
| 303 | + latex, | |
| 304 | + mergedRegion.getFirstRow(), | |
| 305 | + mergedRegion.getFirstColumn(), | |
| 306 | + mergedRegion.getLastRow() - mergedRegion.getFirstRow() + 1, | |
| 307 | + mergedRegion.getLastColumn() - mergedRegion.getFirstColumn() + 1, | |
| 308 | + baseFontSize | |
| 309 | + ); | |
| 310 | + return; | |
| 311 | + } | |
| 312 | + insertContractLatexImageToRegionFillTemplate(workbook, sheet, latex, rowIndex, colIndex, 1, 1, baseFontSize); | |
| 313 | + } | |
| 314 | + | |
| 256 | 315 | private static final int EMU_PER_PX = 9525; |
| 257 | 316 | |
| 258 | 317 | public static void insertLatexImageToCellWithMaxScale(Workbook workbook, Sheet sheet, String latex, int rowIndex, int colIndex, |
| ... | ... | @@ -417,6 +476,96 @@ public class LatexFormulaExcelExporterUtil { |
| 417 | 476 | drawing.createPicture(anchor, pictureIdx); |
| 418 | 477 | } |
| 419 | 478 | |
| 479 | + private static void insertContractLatexImageToRegionFillTemplate(Workbook workbook, Sheet sheet, String latex, | |
| 480 | + int rowIndex, int colIndex, int rowRange, int colRange, | |
| 481 | + int baseFontSize) throws IOException { | |
| 482 | + int regionWidthPx = 0; | |
| 483 | + for (int c = colIndex; c < colIndex + colRange; c++) { | |
| 484 | + regionWidthPx += getColumnWidthPx(sheet, c); | |
| 485 | + } | |
| 486 | + int regionHeightPx = 0; | |
| 487 | + for (int r = rowIndex; r < rowIndex + rowRange; r++) { | |
| 488 | + regionHeightPx += getRowHeightPx(sheet, r); | |
| 489 | + } | |
| 490 | + | |
| 491 | + int paddingXPx = 4; | |
| 492 | + int paddingYPx = 2; | |
| 493 | + int availableWidthPx = Math.max(1, regionWidthPx - paddingXPx * 2); | |
| 494 | + int availableHeightPx = Math.max(1, regionHeightPx - paddingYPx * 2); | |
| 495 | + RenderedLatexImage renderedImage = renderBestFitLatexImageByHeight( | |
| 496 | + latex, | |
| 497 | + baseFontSize, | |
| 498 | + availableHeightPx | |
| 499 | + ); | |
| 500 | + if (renderedImage.image == null) { | |
| 501 | + return; | |
| 502 | + } | |
| 503 | + | |
| 504 | + byte[] imageBytes = renderedImage.imageBytes; | |
| 505 | + BufferedImage image = renderedImage.image; | |
| 506 | + int scaledHeightPx = availableHeightPx; | |
| 507 | + int widthByHeightPx = Math.max(1, (int) Math.round( | |
| 508 | + (double) image.getWidth() * scaledHeightPx / Math.max(1, image.getHeight()) | |
| 509 | + )); | |
| 510 | + int scaledWidthPx = Math.min(availableWidthPx, widthByHeightPx); | |
| 511 | + | |
| 512 | + int offsetXPx = Math.max(paddingXPx, (regionWidthPx - scaledWidthPx) / 2); | |
| 513 | + int offsetYPx = Math.max(paddingYPx, (regionHeightPx - scaledHeightPx) / 2); | |
| 514 | + | |
| 515 | + int pictureIdx = workbook.addPicture(imageBytes, Workbook.PICTURE_TYPE_PNG); | |
| 516 | + Drawing<?> drawing = sheet.createDrawingPatriarch(); | |
| 517 | + ClientAnchor anchor = workbook.getCreationHelper().createClientAnchor(); | |
| 518 | + anchor.setAnchorType(ClientAnchor.AnchorType.MOVE_DONT_RESIZE); | |
| 519 | + | |
| 520 | + AnchorCoord x1 = resolveAnchorX(sheet, colIndex, colRange, offsetXPx); | |
| 521 | + AnchorCoord x2 = resolveAnchorX(sheet, colIndex, colRange, offsetXPx + scaledWidthPx); | |
| 522 | + AnchorCoord y1 = resolveAnchorY(sheet, rowIndex, rowRange, offsetYPx); | |
| 523 | + AnchorCoord y2 = resolveAnchorY(sheet, rowIndex, rowRange, offsetYPx + scaledHeightPx); | |
| 524 | + | |
| 525 | + anchor.setCol1(x1.index); | |
| 526 | + anchor.setCol2(x2.index); | |
| 527 | + anchor.setRow1(y1.index); | |
| 528 | + anchor.setRow2(y2.index); | |
| 529 | + | |
| 530 | + if (anchor instanceof org.apache.poi.hssf.usermodel.HSSFClientAnchor) { | |
| 531 | + int dx1 = pxToHssfDx(sheet, x1.index, x1.offsetPx); | |
| 532 | + int dx2 = pxToHssfDx(sheet, x2.index, x2.offsetPx); | |
| 533 | + int dy1 = pxToHssfDy(sheet, y1.index, y1.offsetPx); | |
| 534 | + int dy2 = pxToHssfDy(sheet, y2.index, y2.offsetPx); | |
| 535 | + | |
| 536 | + if (x2.index == x1.index && dx2 <= dx1) { | |
| 537 | + dx2 = Math.min(1023, dx1 + 1); | |
| 538 | + } | |
| 539 | + if (y2.index == y1.index && dy2 <= dy1) { | |
| 540 | + dy2 = Math.min(255, dy1 + 1); | |
| 541 | + } | |
| 542 | + | |
| 543 | + anchor.setDx1(dx1); | |
| 544 | + anchor.setDx2(dx2); | |
| 545 | + anchor.setDy1(dy1); | |
| 546 | + anchor.setDy2(dy2); | |
| 547 | + } else { | |
| 548 | + int dx1 = x1.offsetPx * EMU_PER_PX; | |
| 549 | + int dx2 = x2.offsetPx * EMU_PER_PX; | |
| 550 | + int dy1 = y1.offsetPx * EMU_PER_PX; | |
| 551 | + int dy2 = y2.offsetPx * EMU_PER_PX; | |
| 552 | + | |
| 553 | + if (x2.index == x1.index && dx2 <= dx1) { | |
| 554 | + dx2 = dx1 + 1; | |
| 555 | + } | |
| 556 | + if (y2.index == y1.index && dy2 <= dy1) { | |
| 557 | + dy2 = dy1 + 1; | |
| 558 | + } | |
| 559 | + | |
| 560 | + anchor.setDx1(dx1); | |
| 561 | + anchor.setDx2(dx2); | |
| 562 | + anchor.setDy1(dy1); | |
| 563 | + anchor.setDy2(dy2); | |
| 564 | + } | |
| 565 | + | |
| 566 | + drawing.createPicture(anchor, pictureIdx); | |
| 567 | + } | |
| 568 | + | |
| 420 | 569 | private static int getColumnWidthPx(Sheet sheet, int colIndex) { |
| 421 | 570 | int widthUnits = sheet.getColumnWidth(colIndex); |
| 422 | 571 | return Math.max(1, (int) Math.round(widthUnits / 256.0 * 7.0 + 5)); |
| ... | ... | @@ -428,6 +577,148 @@ public class LatexFormulaExcelExporterUtil { |
| 428 | 577 | return Math.max(1, (int) Math.round(points * 96.0 / 72.0)); |
| 429 | 578 | } |
| 430 | 579 | |
| 580 | + private static RenderedLatexImage renderBestFitLatexImageByHeight(String latex, int preferredFontSize, | |
| 581 | + int targetHeightPx) throws IOException { | |
| 582 | + int minFontSize = 8; | |
| 583 | + int maxFontSize = Math.max(minFontSize, preferredFontSize + 40); | |
| 584 | + maxFontSize = Math.min(96, maxFontSize); | |
| 585 | + int left = minFontSize; | |
| 586 | + int right = maxFontSize; | |
| 587 | + RenderedLatexImage best = renderLatexImage(latex, minFontSize); | |
| 588 | + | |
| 589 | + while (left <= right) { | |
| 590 | + int mid = (left + right) >>> 1; | |
| 591 | + RenderedLatexImage candidate = renderLatexImage(latex, mid); | |
| 592 | + if (candidate.image != null && candidate.image.getHeight() <= targetHeightPx) { | |
| 593 | + best = candidate; | |
| 594 | + left = mid + 1; | |
| 595 | + } else { | |
| 596 | + right = mid - 1; | |
| 597 | + } | |
| 598 | + } | |
| 599 | + return best; | |
| 600 | + } | |
| 601 | + | |
| 602 | + private static RenderedLatexImage renderLatexImage(String latex, int fontSize) throws IOException { | |
| 603 | + byte[] imageBytes = latexToContractImageBytes(latex, fontSize); | |
| 604 | + BufferedImage image; | |
| 605 | + try (ByteArrayInputStream in = new ByteArrayInputStream(imageBytes)) { | |
| 606 | + image = ImageIO.read(in); | |
| 607 | + } | |
| 608 | + return new RenderedLatexImage(imageBytes, image); | |
| 609 | + } | |
| 610 | + | |
| 611 | + private static byte[] latexToContractImageBytes(String latex, int fontSize) throws IOException { | |
| 612 | + try { | |
| 613 | + double scale = 5.0; | |
| 614 | + TeXFormula formula = new TeXFormula(latex); | |
| 615 | + TeXIcon icon = formula.createTeXIcon(TeXConstants.STYLE_DISPLAY, fontSize); | |
| 616 | + | |
| 617 | + int baseWidth = icon.getIconWidth(); | |
| 618 | + int baseHeight = icon.getIconHeight(); | |
| 619 | + if (baseWidth <= 0 || baseHeight <= 0) { | |
| 620 | + return new byte[0]; | |
| 621 | + } | |
| 622 | + | |
| 623 | + int maxWidth = 4200; | |
| 624 | + int maxHeight = 3200; | |
| 625 | + double widthScale = (double) maxWidth / baseWidth; | |
| 626 | + double heightScale = (double) maxHeight / baseHeight; | |
| 627 | + scale = Math.min(scale, Math.min(widthScale, heightScale)); | |
| 628 | + scale = Math.max(1.0, scale); | |
| 629 | + | |
| 630 | + int scaledWidth = Math.max(1, (int) Math.ceil(baseWidth * scale)); | |
| 631 | + int scaledHeight = Math.max(1, (int) Math.ceil(baseHeight * scale)); | |
| 632 | + BufferedImage image = new BufferedImage(scaledWidth, scaledHeight, BufferedImage.TYPE_INT_RGB); | |
| 633 | + Graphics2D g2 = image.createGraphics(); | |
| 634 | + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); | |
| 635 | + g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); | |
| 636 | + g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); | |
| 637 | + g2.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); | |
| 638 | + g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE); | |
| 639 | + g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); | |
| 640 | + | |
| 641 | + java.awt.Font renderFont = new java.awt.Font("宋体", java.awt.Font.BOLD, fontSize); | |
| 642 | + g2.setFont(renderFont); | |
| 643 | + g2.setColor(Color.WHITE); | |
| 644 | + g2.fillRect(0, 0, scaledWidth, scaledHeight); | |
| 645 | + g2.scale(scale, scale); | |
| 646 | + | |
| 647 | + JLabel label = new JLabel(); | |
| 648 | + label.setFont(renderFont); | |
| 649 | + label.setForeground(Color.BLACK); | |
| 650 | + icon.paintIcon(label, g2, 0, 0); | |
| 651 | + g2.dispose(); | |
| 652 | + | |
| 653 | + image = trimWhitespace(image, 248, 0); | |
| 654 | + ByteArrayOutputStream baos = new ByteArrayOutputStream(); | |
| 655 | + ImageIO.write(image, "png", baos); | |
| 656 | + return baos.toByteArray(); | |
| 657 | + } catch (Exception e) { | |
| 658 | + throw new IOException("合同规格图片渲染失败: " + e.getMessage(), e); | |
| 659 | + } | |
| 660 | + } | |
| 661 | + | |
| 662 | + private static BufferedImage trimWhitespace(BufferedImage source, int whiteThreshold, int marginPx) { | |
| 663 | + if (source == null || source.getWidth() <= 0 || source.getHeight() <= 0) { | |
| 664 | + return source; | |
| 665 | + } | |
| 666 | + | |
| 667 | + int minX = source.getWidth(); | |
| 668 | + int minY = source.getHeight(); | |
| 669 | + int maxX = -1; | |
| 670 | + int maxY = -1; | |
| 671 | + | |
| 672 | + for (int y = 0; y < source.getHeight(); y++) { | |
| 673 | + for (int x = 0; x < source.getWidth(); x++) { | |
| 674 | + if (!isNearWhite(source.getRGB(x, y), whiteThreshold)) { | |
| 675 | + minX = Math.min(minX, x); | |
| 676 | + minY = Math.min(minY, y); | |
| 677 | + maxX = Math.max(maxX, x); | |
| 678 | + maxY = Math.max(maxY, y); | |
| 679 | + } | |
| 680 | + } | |
| 681 | + } | |
| 682 | + | |
| 683 | + if (maxX < minX || maxY < minY) { | |
| 684 | + return source; | |
| 685 | + } | |
| 686 | + | |
| 687 | + int cropMinX = Math.max(0, minX - marginPx); | |
| 688 | + int cropMinY = Math.max(0, minY - marginPx); | |
| 689 | + int cropMaxX = Math.min(source.getWidth() - 1, maxX + marginPx); | |
| 690 | + int cropMaxY = Math.min(source.getHeight() - 1, maxY + marginPx); | |
| 691 | + int cropWidth = cropMaxX - cropMinX + 1; | |
| 692 | + int cropHeight = cropMaxY - cropMinY + 1; | |
| 693 | + | |
| 694 | + if (cropWidth >= source.getWidth() && cropHeight >= source.getHeight()) { | |
| 695 | + return source; | |
| 696 | + } | |
| 697 | + | |
| 698 | + BufferedImage trimmed = new BufferedImage(cropWidth, cropHeight, BufferedImage.TYPE_INT_RGB); | |
| 699 | + Graphics2D g2 = trimmed.createGraphics(); | |
| 700 | + g2.drawImage(source.getSubimage(cropMinX, cropMinY, cropWidth, cropHeight), 0, 0, null); | |
| 701 | + g2.dispose(); | |
| 702 | + return trimmed; | |
| 703 | + } | |
| 704 | + | |
| 705 | + private static boolean isNearWhite(int rgb, int whiteThreshold) { | |
| 706 | + int red = (rgb >> 16) & 0xFF; | |
| 707 | + int green = (rgb >> 8) & 0xFF; | |
| 708 | + int blue = rgb & 0xFF; | |
| 709 | + return red >= whiteThreshold && green >= whiteThreshold && blue >= whiteThreshold; | |
| 710 | + } | |
| 711 | + | |
| 712 | + private static class RenderedLatexImage { | |
| 713 | + final byte[] imageBytes; | |
| 714 | + final BufferedImage image; | |
| 715 | + | |
| 716 | + private RenderedLatexImage(byte[] imageBytes, BufferedImage image) { | |
| 717 | + this.imageBytes = imageBytes; | |
| 718 | + this.image = image; | |
| 719 | + } | |
| 720 | + } | |
| 721 | + | |
| 431 | 722 | private static class AnchorCoord { |
| 432 | 723 | final int index; |
| 433 | 724 | final int offsetPx; | ... | ... |
No preview for this file type
No preview for this file type
No preview for this file type