|
|
@@ -19,6 +19,7 @@ import java.io.IOException;
|
|
|
import java.io.InputStream;
|
|
|
import java.util.*;
|
|
|
import java.util.List;
|
|
|
+
|
|
|
/**
|
|
|
* GrapeCity 控制器
|
|
|
* 提供 Excel 模板处理和 PDF 生成功能
|
|
|
@@ -158,33 +159,24 @@ public class GrapeCityController {
|
|
|
private void collectFloatingImages(IWorksheet worksheet, Set<String> imagePaths, Map<String, Object> data) {
|
|
|
// 这里可以添加收集浮动图片的逻辑,如果需要的话
|
|
|
// 目前 pdf 方法中有处理 Illustration 字段,但 filePath 可能不需要
|
|
|
- if (data.get("Illustration") != null) {
|
|
|
- Object illustrationObj = data.get("Illustration");
|
|
|
- JSONArray jsonArray;
|
|
|
+ Object illustrationObj = data.get("Illustration");
|
|
|
+ if (illustrationObj == null) return;
|
|
|
|
|
|
- // 处理不同类型的 Illustration 数据
|
|
|
- if (illustrationObj instanceof JSONArray arr) {
|
|
|
- jsonArray = arr;
|
|
|
- } else if (illustrationObj instanceof String str && str.trim().startsWith("[")) {
|
|
|
- jsonArray = JSON.parseArray(str);
|
|
|
- } else {
|
|
|
- logger.warn("Illustration 字段格式不正确:{}", illustrationObj.getClass().getName());
|
|
|
- return;
|
|
|
- }
|
|
|
+ JSONArray jsonArray = toJSONArray(illustrationObj);
|
|
|
+ if (jsonArray == null || jsonArray.isEmpty()) return;
|
|
|
|
|
|
- if (jsonArray != null) {
|
|
|
- List<JSONObject> list = new ArrayList<>();
|
|
|
- for (int k = 0; k < jsonArray.size(); k++) {
|
|
|
- JSONObject jsonObject = jsonArray.getJSONObject(k);
|
|
|
- if (jsonObject.getString("sheet").equals(worksheet.getName())) {
|
|
|
- imagePaths.add(jsonObject.getString("url"));
|
|
|
- } else {
|
|
|
- list.add(jsonObject);
|
|
|
- }
|
|
|
- }
|
|
|
- data.put("Illustration", JSON.toJSONString(list));
|
|
|
+ List<JSONObject> list = new ArrayList<>();
|
|
|
+ for (int k = 0; k < jsonArray.size(); k++) {
|
|
|
+ JSONObject jsonObject = jsonArray.getJSONObject(k);
|
|
|
+ if (jsonObject.getString("sheet").equals(worksheet.getName())) {
|
|
|
+ imagePaths.add(jsonObject.getString("url"));
|
|
|
+ } else {
|
|
|
+ list.add(jsonObject);
|
|
|
}
|
|
|
}
|
|
|
+ data.put("Illustration", JSON.toJSONString(list));
|
|
|
+
|
|
|
+
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -224,7 +216,6 @@ public class GrapeCityController {
|
|
|
workbook = new Workbook(workbookOptions);
|
|
|
inputStream = new ByteArrayInputStream(templateBytes);
|
|
|
workbook.open(inputStream, OpenFileFormat.Sjs);
|
|
|
-
|
|
|
for (int i = 0; i < workbook.getWorksheets().getCount(); i++) {
|
|
|
IWorksheet worksheet = workbook.getWorksheets().get(i);
|
|
|
worksheet.getPageSetup().setPrintHeadings(false);
|
|
|
@@ -248,6 +239,19 @@ public class GrapeCityController {
|
|
|
// 浮动图片
|
|
|
processFloatingImages(worksheet, data, fileBytes);
|
|
|
}
|
|
|
+ try {
|
|
|
+ boolean continuePage = request.get("continuePage") != null && Boolean.parseBoolean(request.get("continuePage").toString());
|
|
|
+ String copyConfigJson = request.get("copyConfig") != null
|
|
|
+ ? request.get("copyConfig").toString()
|
|
|
+ : null;
|
|
|
+ List<String> newColCodes = new ArrayList<>();
|
|
|
+ List<String> existingColCodes = extractStringList(request.get("existingColCodes"));
|
|
|
+ if (continuePage && copyConfigJson != null) {
|
|
|
+ workbook = handleContinuationPages(workbook, copyConfigJson, existingColCodes, newColCodes);
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ logger.error("处理分页异常:{}", e.getMessage());
|
|
|
+ }
|
|
|
|
|
|
PrintManager printManager = new PrintManager();
|
|
|
//Workbook.FontsFolderPath = this.fontsFolderPath;
|
|
|
@@ -266,6 +270,7 @@ public class GrapeCityController {
|
|
|
};
|
|
|
PdfSaveOptions pdfOptions = new PdfSaveOptions();
|
|
|
pdfOptions.setIncludeAutoMergedCells(true);
|
|
|
+ pdfOptions.setPrintBackgroundPicture(true);
|
|
|
// pdfOptions.getShrinkToFitSettings().setCanShrinkToFitWrappedText(true);
|
|
|
List<PageInfo> pages = printManager.paginate(workbook);
|
|
|
printManager.savePageInfosToPDF(byteArrayOutputStream, pages, pdfOptions);
|
|
|
@@ -293,7 +298,7 @@ public class GrapeCityController {
|
|
|
* 处理单元格背景图片
|
|
|
*/
|
|
|
private void processCellBackgroundImages(IWorksheet worksheet, Map<String, byte[]> fileBytes) {
|
|
|
- if (fileBytes == null || fileBytes.isEmpty()){
|
|
|
+ if (fileBytes == null || fileBytes.isEmpty()) {
|
|
|
return;
|
|
|
}
|
|
|
for (int x = 0; x < worksheet.getRowCount(); x++) {
|
|
|
@@ -343,7 +348,7 @@ public class GrapeCityController {
|
|
|
* 处理浮动图片
|
|
|
*/
|
|
|
private void processFloatingImages(IWorksheet worksheet, Map<String, Object> data, Map<String, byte[]> fileBytes) {
|
|
|
- if (fileBytes == null || fileBytes.isEmpty()){
|
|
|
+ if (fileBytes == null || fileBytes.isEmpty()) {
|
|
|
return;
|
|
|
}
|
|
|
Object illustrationObj = data.get("Illustration");
|
|
|
@@ -351,6 +356,8 @@ public class GrapeCityController {
|
|
|
JSONArray jsonArray;
|
|
|
if (illustrationObj instanceof JSONArray arr) {
|
|
|
jsonArray = arr;
|
|
|
+ } else if (illustrationObj instanceof List arr) {
|
|
|
+ jsonArray = JSON.parseArray(JSON.toJSONString(arr));
|
|
|
} else if (illustrationObj instanceof String str && str.trim().startsWith("[")) {
|
|
|
jsonArray = JSON.parseArray(str);
|
|
|
} else {
|
|
|
@@ -364,21 +371,39 @@ public class GrapeCityController {
|
|
|
if (jsonObject.getString("sheet").equals(worksheet.getName())) {
|
|
|
String url = jsonObject.getString("url");
|
|
|
byte[] bytes = fileBytes.get(url);
|
|
|
- if (bytes != null) {
|
|
|
- try (InputStream byteArrayInputStream = new ByteArrayInputStream(bytes)) {
|
|
|
- worksheet.getShapes().addPictureInPixel(
|
|
|
- byteArrayInputStream,
|
|
|
- ImageType.JPG,
|
|
|
- jsonObject.getDouble("x"),
|
|
|
- jsonObject.getDouble("y"),
|
|
|
- jsonObject.getDouble("width"),
|
|
|
- jsonObject.getDouble("height")
|
|
|
- );
|
|
|
- } catch (IOException e) {
|
|
|
- logger.warn("添加浮动图片失败:{}", url, e);
|
|
|
- }
|
|
|
- } else {
|
|
|
- logger.warn("未找到浮动图片数据:{}", url);
|
|
|
+
|
|
|
+ if (bytes == null || bytes.length == 0) {
|
|
|
+ logger.warn("浮动图片数据为空: {}", url);
|
|
|
+ list.add(jsonObject);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!isValidImage(bytes)) {
|
|
|
+ logger.error("浮动图片损坏或格式不支持: {}", url);
|
|
|
+ list.add(jsonObject);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ ImageType imageType = detectImageType(bytes);
|
|
|
+ if (imageType == null) {
|
|
|
+ logger.error("无法识别浮动图片格式: {}", url);
|
|
|
+ list.add(jsonObject);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ InputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
|
|
|
+ try {
|
|
|
+ worksheet.getShapes().addPictureInPixel(
|
|
|
+ url,
|
|
|
+ byteArrayInputStream,
|
|
|
+ imageType,
|
|
|
+ jsonObject.getDouble("x"),
|
|
|
+ jsonObject.getDouble("y"),
|
|
|
+ jsonObject.getDouble("width"),
|
|
|
+ jsonObject.getDouble("height")
|
|
|
+ );
|
|
|
+ } catch (IOException e) {
|
|
|
+ logger.error("图片流异常: {}", url, e);
|
|
|
}
|
|
|
} else {
|
|
|
list.add(jsonObject);
|
|
|
@@ -389,6 +414,123 @@ public class GrapeCityController {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 处理续页:检查 copyConfig 配置,处理隐藏空页和自动生成续页
|
|
|
+ * copyConfigJson 和 existingColCodes 由 pressure2 传入,newColCodes 通过参数返回给调用方
|
|
|
+ */
|
|
|
+ private Workbook handleContinuationPages(Workbook workbook, String copyConfigJson,
|
|
|
+ List<String> existingColCodes, List<String> newColCodes) {
|
|
|
+ try {
|
|
|
+ JSONObject copyConfig = JSON.parseObject(copyConfigJson);
|
|
|
+
|
|
|
+ Boolean isContinuePage = copyConfig.getBoolean("isContinuePage");
|
|
|
+ if (!Boolean.TRUE.equals(isContinuePage)) return workbook;
|
|
|
+
|
|
|
+ String sheetName = copyConfig.getString("sheetName");
|
|
|
+ if (sheetName == null) return workbook;
|
|
|
+
|
|
|
+ JSONObject copyRange = copyConfig.getJSONObject("copyRange");
|
|
|
+ if (copyRange == null) return workbook;
|
|
|
+
|
|
|
+ String[] topLeft = copyRange.getString("topLeft").split(",");
|
|
|
+ String[] topRight = copyRange.getString("topRight").split(",");
|
|
|
+ String[] bottomLeft = copyRange.getString("bottomLeft").split(",");
|
|
|
+
|
|
|
+ int col = Integer.parseInt(topLeft[0]);
|
|
|
+ int row = Integer.parseInt(topLeft[1]);
|
|
|
+ int colCount = Integer.parseInt(topRight[0]) - col;
|
|
|
+ int rowCount = Integer.parseInt(bottomLeft[1]) - row;
|
|
|
+
|
|
|
+ WorkbookOptions workbookOptions = new WorkbookOptions();
|
|
|
+ workbookOptions.setPixelBasedColumnWidth(true);
|
|
|
+ IWorksheet targetSheet = null;
|
|
|
+ for (int i = 0; i < workbook.getWorksheets().getCount(); i++) {
|
|
|
+ if (workbook.getWorksheets().get(i).getName().equals(sheetName)) {
|
|
|
+ targetSheet = workbook.getWorksheets().get(i);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (targetSheet == null) return workbook;
|
|
|
+
|
|
|
+ Boolean hidden = copyConfig.getBoolean("hidden");
|
|
|
+ if (Boolean.TRUE.equals(hidden)) {
|
|
|
+ boolean isEmpty = true;
|
|
|
+ for (int i = 0; i <= colCount; i++) {
|
|
|
+ Object cellValue = targetSheet.getRange(row, col + i).getValue();
|
|
|
+ if (cellValue != null && !cellValue.toString().isEmpty()) {
|
|
|
+ isEmpty = false;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (isEmpty) {
|
|
|
+ targetSheet.setVisible(Visibility.Hidden);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ List<String> effectiveExistingColCodes = existingColCodes != null ? existingColCodes : new ArrayList<>();
|
|
|
+
|
|
|
+ int sheetNum = 2;
|
|
|
+
|
|
|
+ boolean isNotEmpty = false;
|
|
|
+ for (int i = 0; i <= colCount; i++) {
|
|
|
+ Object cellValue = targetSheet.getRange(row + rowCount, col + i).getValue();
|
|
|
+ if (cellValue != null && !cellValue.toString().isEmpty()) {
|
|
|
+ isNotEmpty = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isNotEmpty) {
|
|
|
+ generateContinuedSheet(workbook, targetSheet, sheetName, sheetNum,
|
|
|
+ col, row, colCount, rowCount, effectiveExistingColCodes, newColCodes);
|
|
|
+ sheetNum++;
|
|
|
+ }
|
|
|
+
|
|
|
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
|
|
+ workbook.save(outputStream, SaveFileFormat.Sjs);
|
|
|
+ return workbook;
|
|
|
+ } catch (Exception e) {
|
|
|
+ logger.warn("续页处理失败,回退返回原始 SJS", e);
|
|
|
+ return workbook;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成续页工作表:复制 sheet 并调整绑定路径
|
|
|
+ * 新字段编码通过 newColCodes 参数返回给调用方
|
|
|
+ */
|
|
|
+ private void generateContinuedSheet(Workbook workbook, IWorksheet sourceSheet,
|
|
|
+ String sheetName, int sheetNum,
|
|
|
+ int col, int row, int colCount, int rowCount,
|
|
|
+ List<String> existingColCodes, List<String> newColCodes) {
|
|
|
+ String sheetJson = sourceSheet.toJson();
|
|
|
+ IWorksheet newSheet = workbook.getWorksheets().add();
|
|
|
+ newSheet.fromJson(sheetJson);
|
|
|
+ String newSheetName = sheetName + " (" + sheetNum + ")";
|
|
|
+ newSheet.setName(newSheetName);
|
|
|
+
|
|
|
+ for (int i = 0; i <= colCount; i++) {
|
|
|
+ for (int j = 0; j <= rowCount; j++) {
|
|
|
+ String bindingPath = sourceSheet.getRange(row + j, col + i).getBindingPath();
|
|
|
+ if (bindingPath != null && bindingPath.contains("_")) {
|
|
|
+ String prefix = bindingPath.substring(0, bindingPath.lastIndexOf("_") + 1);
|
|
|
+ String numStr = bindingPath.substring(bindingPath.lastIndexOf("_") + 1);
|
|
|
+ try {
|
|
|
+ int num = Integer.parseInt(numStr);
|
|
|
+ int newNum = num + (rowCount + 1) * (sheetNum - 1);
|
|
|
+ String newPath = prefix + newNum;
|
|
|
+ newSheet.getRange(row + j, col + i).setBindingPath(newPath);
|
|
|
+ if (!existingColCodes.contains(newPath) && !newColCodes.contains(newPath)) {
|
|
|
+ newColCodes.add(newPath);
|
|
|
+ }
|
|
|
+ } catch (NumberFormatException ignored) {
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
/**
|
|
|
* 合并图片
|
|
|
* 将多张图片合并成一个图片,高度压缩成一致
|
|
|
@@ -662,4 +804,68 @@ public class GrapeCityController {
|
|
|
tableRange.setWrapText(true);
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ private List<String> extractStringList(Object obj) {
|
|
|
+ if (obj == null) return new ArrayList<>();
|
|
|
+ if (obj instanceof List) return (List<String>) obj;
|
|
|
+ return new ArrayList<>();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将对象安全转换为 JSONArray
|
|
|
+ * 兼容 fastjson2 JSONArray 和 Jackson 反序列化的 ArrayList
|
|
|
+ */
|
|
|
+ private JSONArray toJSONArray(Object obj) {
|
|
|
+ if (obj instanceof JSONArray arr) return arr;
|
|
|
+ if (obj instanceof List<?> list) {
|
|
|
+ JSONArray arr = new JSONArray();
|
|
|
+ arr.addAll(list);
|
|
|
+ return arr;
|
|
|
+ }
|
|
|
+ if (obj instanceof String str && str.trim().startsWith("[")) {
|
|
|
+ try {
|
|
|
+ return JSON.parseArray(str);
|
|
|
+ } catch (Exception ignored) {
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean isValidImage(byte[] imageBytes) {
|
|
|
+ if (imageBytes == null || imageBytes.length < 4) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ BufferedImage image = ImageIO.read(new ByteArrayInputStream(imageBytes));
|
|
|
+ return image != null;
|
|
|
+ } catch (Exception e) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private ImageType detectImageType(byte[] imageBytes) {
|
|
|
+ if (imageBytes == null || imageBytes.length < 4) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ int firstByte = imageBytes[0] & 0xFF;
|
|
|
+ int secondByte = imageBytes[1] & 0xFF;
|
|
|
+
|
|
|
+ if (firstByte == 0xFF && secondByte == 0xD8) {
|
|
|
+ return ImageType.JPEG;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (firstByte == 0x89 && secondByte == 0x50) {
|
|
|
+ return ImageType.PNG;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (firstByte == 0x47 && secondByte == 0x49) {
|
|
|
+ return ImageType.GIF;
|
|
|
+ }
|
|
|
+
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
}
|