diff --git a/pdf-generator/README.md b/pdf-generator/README.md
new file mode 100644
index 000000000..0866548e7
--- /dev/null
+++ b/pdf-generator/README.md
@@ -0,0 +1,5 @@
+支持动态生成pdf
+支持xlsx转pdf
+
+文档内容维护在:
+https://community.codewave.163.com/CommunityParent/CodeWareMarketLibraryDetail?id=2931140592946432&isLatest=false&isClassics=false
\ No newline at end of file
diff --git a/pdf-generator/pom.xml b/pdf-generator/pom.xml
new file mode 100644
index 000000000..c774c2e38
--- /dev/null
+++ b/pdf-generator/pom.xml
@@ -0,0 +1,154 @@
+
+
+ 4.0.0
+
+ com.netease.lowcode.extension
+ pdf-generator
+ 0.0.23
+
+
+ 8
+ 8
+ UTF-8
+
+
+
+ nasl-metadata-collector
+ com.netease.lowcode
+ 0.8.0
+ true
+
+
+
+
+ com.itextpdf
+ itextpdf
+ 5.5.10
+
+
+ com.itextpdf
+ itext-asian
+ 5.2.0
+
+
+
+
+
+ com.itextpdf
+ itext-core
+ 8.0.4
+ pom
+
+
+
+ org.apache.poi
+ poi
+ 4.1.2
+
+
+ org.apache.poi
+ poi-ooxml
+ 4.1.2
+
+
+ org.apache.poi
+ poi-ooxml-schemas
+ 4.1.2
+
+
+ org.apache.poi
+ poi-scratchpad
+ 4.1.2
+
+
+ org.apache.poi
+ poi-excelant
+ 4.1.2
+
+
+
+
+ org.apache.commons
+ commons-collections4
+ 4.4
+
+
+
+ org.apache.commons
+ commons-lang3
+ 3.14.0
+
+
+
+ com.alibaba.fastjson2
+ fastjson2
+ 2.0.51
+
+
+
+ org.freemarker
+ freemarker
+ 2.3.31
+
+
+ com.squareup.okhttp3
+ okhttp
+ 3.14.9
+
+
+
+ org.springframework
+ spring-context
+ 5.2.8.RELEASE
+ provided
+
+
+ org.springframework
+ spring-web
+ 5.2.8.RELEASE
+ provided
+
+
+ org.apache.tomcat.embed
+ tomcat-embed-core
+ 9.0.37
+ provided
+
+
+
+
+
+
+ com.netease.lowcode
+ nasl-metadata-maven-plugin
+ 1.4.1
+
+
+ org.apache.maven.shared
+ maven-common-artifact-filters
+ 3.0.1
+
+
+ org.apache.commons
+ commons-lang3
+ 3.13.0
+
+
+
+
+
+ archive
+
+
+
+
+
+ false
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/pdf-generator/src/main/java/com/netease/lowcode/pdf/extension/Excel2Pdf.java b/pdf-generator/src/main/java/com/netease/lowcode/pdf/extension/Excel2Pdf.java
new file mode 100644
index 000000000..4118c31cb
--- /dev/null
+++ b/pdf-generator/src/main/java/com/netease/lowcode/pdf/extension/Excel2Pdf.java
@@ -0,0 +1,439 @@
+package com.netease.lowcode.pdf.extension;
+
+import com.alibaba.fastjson2.JSONArray;
+import com.alibaba.fastjson2.JSONObject;
+import com.netease.lowcode.core.annotation.NaslLogic;
+import com.netease.lowcode.pdf.extension.structures.BaseResponse;
+import com.netease.lowcode.pdf.extension.structures.CreateByXlsxRequest;
+import com.netease.lowcode.pdf.extension.utils.FileUtils;
+import com.netease.lowcode.pdf.extension.utils.JSONObjectUtil;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.poi.ss.usermodel.*;
+import org.apache.poi.ss.util.CellRangeAddress;
+import org.apache.poi.xssf.usermodel.XSSFColor;
+import org.apache.poi.xssf.usermodel.XSSFFont;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.*;
+
+public class Excel2Pdf {
+
+
+ @NaslLogic
+ public static BaseResponse xlsx2pdf(CreateByXlsxRequest request) {
+
+ try {
+ if (StringUtils.isBlank(request.getExportFileName()) || !request.getExportFileName().endsWith(".pdf")) {
+ return BaseResponse.FAIL("exportFileName必须以 .pdf 结尾");
+ }
+
+ // 下载模板文件
+ File templateFile = FileUtils.downloadFile(request.getTemplateUrl());
+
+ String fileName = templateFile.getName();
+ InputStream inputStream = new FileInputStream(templateFile);
+
+ Workbook wb = null;
+ if (fileName.endsWith(".xlsx")) {
+ // 解析 *.xlsx
+ wb = new XSSFWorkbook(inputStream);
+ } else {
+ return BaseResponse.FAIL("仅支持 *.xlsx 文件");
+ }
+
+ JSONObject jsonObject = new JSONObject();
+ jsonObject.put("fileName",request.getExportFileName());
+ // 字体设置
+ jsonObject.put("font",new HashMap(){{
+ put("fontProgram","STSong-Light");
+ put("encoding","UniGB-UCS2-H");
+ }});
+ // 纸张大小A4
+ jsonObject.put("pageSize",request.getPageSize());
+ // 纸张方向
+ jsonObject.put("rotate",request.getRotate());
+ JSONArray nodes = new JSONArray();
+ jsonObject.put("nodes",nodes);
+
+ // 读取第0个sheet
+ Sheet sheet0 = wb.getSheetAt(0);
+ if (Objects.isNull(sheet0)) {
+ return BaseResponse.FAIL("读取sheet0为空");
+ }
+
+ JSONObject table = new JSONObject();
+ table.put("type","Table");
+ table.put("width",100);
+
+ JSONArray cells = new JSONArray();
+ table.put("cells", cells);
+ nodes.add(table);
+
+ // 记录整个sheet列宽度
+ List sheetColWidthList = new ArrayList<>();
+ // 暂存单元格
+ List> tmpCells = new ArrayList<>();
+
+ // 遍历sheet行
+ for (int i = 0; i <= sheet0.getLastRowNum(); i++) {
+ Row row = sheet0.getRow(i);
+ // 暂存该行单元格
+ List curRowTmpCells = new ArrayList<>();
+ tmpCells.add(curRowTmpCells);
+
+ if(Objects.isNull(row)){
+ // 保留空行
+ continue;
+ }
+
+ List currentRowColWidths = new ArrayList<>();
+ // 遍历列,注意模板不要超过A4的宽度
+ for (int j = 0; j < row.getLastCellNum(); j++) {
+
+ JSONObject jsonCell = new JSONObject();
+ JSONArray elements = new JSONArray();
+ JSONObject paragraph = new JSONObject();
+ paragraph.put("type","Paragraph");
+ elements.add(paragraph);
+ jsonCell.put("elements", elements);
+
+ curRowTmpCells.add(jsonCell);
+
+ // 获取单元格
+ Cell cell = row.getCell(j);
+
+ if (Objects.isNull(cell)) {
+ currentRowColWidths.add(0);
+ continue;
+ }
+
+ currentRowColWidths.add(sheet0.getColumnWidth(j));
+
+ // 单元格字体
+ Font font = wb.getFontAt(cell.getCellStyle().getFontIndexAsInt());
+ // 字体颜色
+ if (font instanceof XSSFFont) {
+ XSSFFont xssfFont = (XSSFFont) font;
+ XSSFColor xssfColor = xssfFont.getXSSFColor();
+ if(Objects.nonNull(xssfColor)) {
+ byte[] rgb = xssfColor.getRGB();
+ paragraph.put("rgb", new HashMap() {
+ {
+ put("red", (rgb[0] < 0) ? (rgb[0] + 256) : rgb[0]);
+ put("green", (rgb[1] < 0) ? (rgb[1] + 256) : rgb[1]);
+ put("blue", (rgb[2] < 0) ? (rgb[2] + 256) : rgb[2]);
+ }
+ });
+ }
+ }
+ // 字体大小
+ short fontSize = font.getFontHeightInPoints();
+ paragraph.put("fontSize",fontSize);
+ // 字体是否加粗
+ boolean bold = font.getBold();
+ paragraph.put("bold", bold);
+ // 下划线
+ byte underline = font.getUnderline();
+
+
+ // 单元格类型
+ CellType cellType = cell.getCellType();
+ if (CellType.STRING.equals(cellType)) {
+ paragraph.put("text", cell.getStringCellValue());
+ }
+
+
+ // 判断水平居中
+ HorizontalAlignment alignment = cell.getCellStyle().getAlignment();
+ if (HorizontalAlignment.CENTER.equals(alignment)) {
+ jsonCell.put("textAlignment", "CENTER");
+ }
+ // 判断垂直居中
+ VerticalAlignment verticalAlignment = cell.getCellStyle().getVerticalAlignment();
+ if (VerticalAlignment.CENTER.equals(verticalAlignment)) {
+
+ }
+
+ // 表格底部边框
+ BorderStyle borderBottom = cell.getCellStyle().getBorderBottom();
+ if (BorderStyle.THIN.equals(borderBottom)) {
+ JSONObject value = new JSONObject();
+ value.put("width", 1);
+ jsonCell.put("borderBottom", value);
+ }
+ BorderStyle borderTop = cell.getCellStyle().getBorderTop();
+ if (BorderStyle.THIN.equals(borderTop)) {
+ JSONObject value = new JSONObject();
+ value.put("width", 1);
+ jsonCell.put("borderTop", value);
+ }
+ BorderStyle borderLeft = cell.getCellStyle().getBorderLeft();
+ if (BorderStyle.THIN.equals(borderLeft)) {
+ JSONObject value = new JSONObject();
+ value.put("width", 1);
+ jsonCell.put("borderLeft", value);
+ }
+ BorderStyle borderRight = cell.getCellStyle().getBorderRight();
+ if (BorderStyle.THIN.equals(borderRight)) {
+ JSONObject value = new JSONObject();
+ value.put("width", 1);
+ jsonCell.put("borderRight", value);
+ }
+
+ }
+
+ // 设置宽度
+ if (currentRowColWidths.size() > sheetColWidthList.size()) {
+ for (int j = 0; j < sheetColWidthList.size(); j++) {
+ if (sheetColWidthList.get(j) == 0) {
+ sheetColWidthList.set(j, currentRowColWidths.get(j));
+ }
+ }
+ int size = sheetColWidthList.size();
+ for (int j = size; j < currentRowColWidths.size(); j++) {
+ sheetColWidthList.add(currentRowColWidths.get(j));
+ }
+ } else {
+ for (int j = 0; j < currentRowColWidths.size(); j++) {
+ if (sheetColWidthList.get(j) == 0) {
+ sheetColWidthList.set(j, currentRowColWidths.get(j));
+ }
+ }
+ }
+ }
+ // 单元格末尾对齐填充
+ int maxSize = tmpCells.stream().mapToInt(List::size).max().orElse(0);
+ for (List tmpRow : tmpCells) {
+ if (tmpRow.size() < maxSize) {
+ int diff = maxSize - tmpRow.size();
+ for (int i = 0; i < diff; i++) {
+ JSONObject jsonCell = new JSONObject();
+ JSONArray elements = new JSONArray();
+ JSONObject paragraph = new JSONObject();
+ paragraph.put("type", "Paragraph");
+ elements.add(paragraph);
+ jsonCell.put("elements", elements);
+ tmpRow.add(jsonCell);
+ }
+ }
+ }
+
+ // 标记合并区域
+ List mergedRegions = sheet0.getMergedRegions();
+ if(CollectionUtils.isNotEmpty(mergedRegions)){
+ for (CellRangeAddress mergedRegion : mergedRegions) {
+ // 得到的单元格坐标是按照合并前的坐标来计算,因此使用cloneTmpCells
+ int firstRow = mergedRegion.getFirstRow();
+ int lastRow = mergedRegion.getLastRow();
+ int firstColumn = mergedRegion.getFirstColumn();
+ int lastColumn = mergedRegion.getLastColumn();
+
+ // 记录合并单元格
+ JSONObject mergeCell = tmpCells.get(firstRow).get(firstColumn);
+ mergeCell.put("rowspan", lastRow - firstRow + 1);
+ mergeCell.put("colspan", lastColumn - firstColumn + 1);
+
+ // 处理合并单元格边框,仅需处理右侧、底部
+ if (tmpCells.get(firstRow).get(lastColumn).containsKey("borderRight")) {
+ mergeCell.put("borderRight", tmpCells.get(firstRow).get(lastColumn).getJSONObject("borderRight"));
+ }
+ if (tmpCells.get(lastRow).get(firstColumn).containsKey("borderLeft")) {
+ mergeCell.put("borderLeft", tmpCells.get(lastRow).get(firstColumn).getJSONObject("borderLeft"));
+ }
+ if (tmpCells.get(lastRow).get(lastColumn).containsKey("borderBottom")) {
+ mergeCell.put("borderBottom", tmpCells.get(lastRow).get(lastColumn).getJSONObject("borderBottom"));
+ }
+
+ // 处理合并区域
+ for (int i = firstRow; i <= lastRow; i++) {
+ List list = tmpCells.get(i);
+ List newList = new ArrayList<>();
+
+ for (int j = 0; j < list.size(); j++) {
+ // 当前行 合并区域处理
+ if (j >= firstColumn && j <= lastColumn) {
+ // 合并区域 首行 第一个单元格 填充mergeCell
+ if (i == firstRow && j == firstColumn) {
+ newList.add(mergeCell);
+ continue;
+ }
+ // 合并区域 其他单元格 用占位标记,后续统一删除
+ JSONObject e = new JSONObject();
+ e.put("mergeTagWillBeDeleted", "");
+ newList.add(e);
+ continue;
+ }
+
+ // 非合并区域 用原始单元格填充
+ newList.add(list.get(j));
+ }
+ // 替换新行
+ tmpCells.set(i, newList);
+ }
+ }
+ }
+ // 移除被合并单元格
+ tmpCells.removeIf(curRow -> {
+ curRow.removeIf(next -> next.containsKey("mergeTagWillBeDeleted"));
+ return curRow.isEmpty();
+ });
+
+ // 从全局设置cell宽度
+ int totalColWidth = sheetColWidthList.stream().mapToInt(Integer::intValue).sum();
+ for (int i = 0; i < tmpCells.size(); i++) {
+ List list = tmpCells.get(i);
+ // 处理该行
+ for (int j = 0; j < list.size(); j++) {
+ JSONObject currentCell = list.get(j);
+ if (currentCell.containsKey("rowspan") && currentCell.containsKey("colspan")) {
+ Integer colspan = currentCell.getInteger("colspan");
+ float value = 0.0f;
+ for (int k = j; k < j + colspan; k++) {
+ value += ((sheetColWidthList.get(k) / (float) totalColWidth) * 100);
+ }
+ currentCell.put("width", Math.round(value));
+ } else {
+ float value = (sheetColWidthList.get(j) / (float) totalColWidth) * 100;
+ currentCell.put("width", Math.round(value));
+ }
+ }
+ }
+
+ // 处理freemarker list
+ handleFreemarkerList(tmpCells,request.getJsonData());
+
+ for (int i = 0; i < tmpCells.size(); i++) {
+ List list = tmpCells.get(i);
+ for (JSONObject object : list) {
+ // 去除单元格默认边框
+ object.put("noBorder", true);
+ }
+ cells.addAll(list);
+ }
+
+ // 设置表格列数
+ table.put("columnSize",maxSize);
+ // 由于将整个sheet解析为一个完整的table,因此不再设置chunkSize
+ // table.put("chunkSize",2);
+
+ BaseResponse response = PdfGenerator.createPDFV2ByStr(request.getJsonData(), jsonObject.toJSONString());
+ if (response.getSuccess()) {
+ return BaseResponse.OK(response.filePath, response.result);
+ }
+
+ return BaseResponse.FAIL(response.trace, "pdf创建失败:" + response.msg);
+ } catch (IOException e) {
+ return BaseResponse.FAIL(Arrays.toString(e.getStackTrace()), e.getMessage());
+ }
+ }
+
+ /**
+ * 处理freemarker list标签
+ *
+ * @param tmpCells
+ */
+ public static void handleFreemarkerList(List> tmpCells, String jsonData) {
+ if (CollectionUtils.isEmpty(tmpCells)) {
+ return;
+ }
+
+ // 解析参数
+ JSONObject requestJsonData = null;
+
+ int i = 0;
+ while (i < tmpCells.size()) {
+ // 当前行
+ List originRow = tmpCells.get(i);
+ if (CollectionUtils.isEmpty(originRow)) {
+ i++;
+ continue;
+ }
+
+ // 判断当前行是否包含freemarker list标签 ${list.arr}
+ boolean hasFreemarkerListTag = false;
+ for (int j = 0; j < originRow.size(); j++) {
+ JSONObject originCell = originRow.get(j);
+ if (isFreemarkerListTag(originCell)) {
+ hasFreemarkerListTag = true;
+ break;
+ }
+ }
+
+ // 包含list标签,开始处理
+ if (hasFreemarkerListTag) {
+ if (Objects.isNull(requestJsonData)) {
+ requestJsonData = JSONObject.parseObject(jsonData);
+ }
+ List> newRows = JSONObjectUtil.fillListData(originRow, requestJsonData);
+ // 将originRow 替换为 newRows
+ for (int j = 0; j < newRows.size(); j++) {
+ tmpCells.add(i + j, newRows.get(j));
+ }
+ // 移除originRow
+ tmpCells.remove(i + newRows.size());
+ i += newRows.size();
+ } else {
+ i++;
+ }
+ }
+ }
+
+ /**
+ * 获取cell内paragraph的text字段值
+ *
+ * @param cell
+ * @return
+ */
+ public static String getCellText(JSONObject cell) {
+ if (Objects.isNull(cell)) {
+ return null;
+ }
+ if (!cell.containsKey("elements")) {
+ return null;
+ }
+ JSONArray elements = cell.getJSONArray("elements");
+ if (Objects.isNull(elements) || elements.isEmpty()) {
+ return null;
+ }
+ JSONObject paragraph = elements.getJSONObject(0);
+ if (!paragraph.containsKey("text")) {
+ return null;
+ }
+ return paragraph.getString("text");
+ }
+
+ /**
+ * 判断一个cell是否为freemarker 的list 标签
+ * 匹配 ${xx.xxx} xx为list变量名,xxx为item属性名
+ *
+ * @param cell
+ * @return
+ */
+ public static boolean isFreemarkerListTag(JSONObject cell) {
+ if (Objects.isNull(cell)) {
+ return false;
+ }
+ if (!cell.containsKey("elements")) {
+ return false;
+ }
+ JSONArray elements = cell.getJSONArray("elements");
+ if (Objects.isNull(elements) || elements.isEmpty()) {
+ return false;
+ }
+ JSONObject paragraph = elements.getJSONObject(0);
+ if (!paragraph.containsKey("text")) {
+ return false;
+ }
+ String text = paragraph.getString("text");
+ if (StringUtils.isNotBlank(text) && text.startsWith("${") && text.endsWith("}") && text.contains(".")) {
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/pdf-generator/src/main/java/com/netease/lowcode/pdf/extension/PdfGenerator.java b/pdf-generator/src/main/java/com/netease/lowcode/pdf/extension/PdfGenerator.java
new file mode 100644
index 000000000..6abdb9226
--- /dev/null
+++ b/pdf-generator/src/main/java/com/netease/lowcode/pdf/extension/PdfGenerator.java
@@ -0,0 +1,298 @@
+package com.netease.lowcode.pdf.extension;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONObject;
+import com.itextpdf.forms.PdfAcroForm;
+import com.itextpdf.forms.fields.PdfFormField;
+import com.itextpdf.kernel.font.PdfFont;
+import com.itextpdf.kernel.font.PdfFontFactory;
+import com.itextpdf.text.*;
+import com.itextpdf.text.pdf.*;
+import com.netease.lowcode.pdf.extension.itextpdf.NodeCreator;
+import com.netease.lowcode.pdf.extension.itextpdf.PdfUtils;
+import com.netease.lowcode.pdf.extension.structures.BaseResponse;
+import com.netease.lowcode.pdf.extension.structures.CreateByTemplateRequest;
+import com.netease.lowcode.pdf.extension.structures.CreateRequest;
+import com.netease.lowcode.pdf.extension.utils.FileUtils;
+import com.netease.lowcode.pdf.extension.utils.FreemarkerUtils;
+import com.netease.lowcode.pdf.extension.utils.UploadResponseDTO;
+import com.netease.lowcode.core.annotation.NaslLogic;
+import freemarker.template.TemplateException;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.collections4.MapUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.stereotype.Component;
+
+import java.io.*;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+// Press Shift twice to open the Search Everywhere dialog and type `show whitespaces`,
+// then press Enter. You can now see whitespace characters in your code.
+@Component("libraryPdfGenerator")
+public class PdfGenerator {
+
+ private static FileUtils fileUtils;
+
+ @Autowired
+ @Qualifier("pdfGeneratorFileUtils")
+ public void setFileUtils(FileUtils fileUtils) {
+ PdfGenerator.fileUtils = fileUtils;
+ }
+
+ @NaslLogic
+ public static BaseResponse createPDFByTemplate(CreateByTemplateRequest request) {
+ String outPath = "data/" + System.currentTimeMillis() + "/";
+ try {
+ // 下载模板文件
+ File file = FileUtils.downloadFile(request.templateUrl);
+
+ // 创建暂存目录
+ File dir = new File(outPath);
+ dir.mkdirs();
+
+ com.itextpdf.kernel.pdf.PdfDocument pdfDocument =
+ new com.itextpdf.kernel.pdf.PdfDocument(
+ new com.itextpdf.kernel.pdf.PdfReader(file),
+ new com.itextpdf.kernel.pdf.PdfWriter(outPath+request.exportFileName)
+ );
+ PdfAcroForm pdfAcroForm = PdfAcroForm.getAcroForm(pdfDocument,true);
+
+ // 获取模板字段
+ Map formFields = pdfAcroForm.getAllFormFields();
+ // 获取填充值
+ Map dataMap = JSON.parseObject(request.jsonData, Map.class);
+
+ formFields.forEach((key,value)->{
+ try {
+ // 处理中文乱码
+ PdfFont font = PdfFontFactory.createFont("STSong-Light","UniGB-UCS2-H");
+ value.setFont(font);
+ value.setValue(dataMap.get(key));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ });
+
+ // 设置不可编辑
+ pdfAcroForm.flattenFields();
+ pdfDocument.close();
+
+ // 上传文件
+ UploadResponseDTO uploadResponseDTO = fileUtils.uploadFileV2(new File(outPath + request.exportFileName));
+
+ return BaseResponse.OK(uploadResponseDTO.getFilePath(), uploadResponseDTO.getResult());
+
+ } catch (IOException | InvocationTargetException | NoSuchMethodException | IllegalAccessException e) {
+ return BaseResponse.FAIL(Arrays.toString(e.getStackTrace()), e.getMessage());
+ }
+
+ }
+
+ /**
+ * 传入的是模板文件url
+ *
+ * @param jsonData
+ * @param templateUrl
+ * @return
+ */
+ @NaslLogic
+ public static BaseResponse createPDFV2(String jsonData, String templateUrl) {
+ try {
+
+ ByteArrayInputStream byteArrayInputStream = FreemarkerUtils.getFreemarkerContentInputStreamV2(jsonData, templateUrl);
+
+ BufferedReader br = new BufferedReader(new InputStreamReader(byteArrayInputStream));
+ StringBuilder sb = new StringBuilder();
+ String line;
+ while ((line = br.readLine()) != null) {
+ sb.append(line);
+ }
+
+ JSONObject jsonObject = JSONObject.parseObject(sb.toString());
+// JSONObject jsonObject = JSONObject.parseObject(PdfUtils.readJson("pdf-generator/src/main/resources/result.json"));
+ ByteArrayOutputStream byteArrayOutputStream = NodeCreator.node(jsonObject);
+ ByteArrayInputStream uploadStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
+
+ UploadResponseDTO uploadResponseDTO = FileUtils.uploadStream(uploadStream, jsonObject.getString("fileName"));
+
+ return BaseResponse.OK(uploadResponseDTO.getFilePath(), uploadResponseDTO.getResult());
+ } catch (Exception e) {
+ return BaseResponse.FAIL(Arrays.toString(e.getStackTrace()), e.getMessage());
+ }
+ }
+
+ /**
+ * 传入的是模板文件json字符串
+ *
+ * @param jsonData
+ * @param jsonTemplate
+ * @return
+ */
+ @NaslLogic
+ public static BaseResponse createPDFV2ByStr(String jsonData, String jsonTemplate) {
+ try {
+ ByteArrayInputStream byteArrayInputStream = FreemarkerUtils.getFreemarkerContentInputStream(jsonData, jsonTemplate);
+ BufferedReader br = new BufferedReader(new InputStreamReader(byteArrayInputStream));
+ StringBuilder sb = new StringBuilder();
+ String line;
+ while ((line = br.readLine()) != null) {
+ sb.append(line);
+ }
+ JSONObject jsonObject = JSONObject.parseObject(sb.toString());
+ ByteArrayOutputStream byteArrayOutputStream = NodeCreator.node(jsonObject);
+ ByteArrayInputStream uploadStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
+
+// TODO:上传文件
+ UploadResponseDTO uploadResponseDTO = FileUtils.uploadStream(uploadStream, jsonObject.getString("fileName"));
+
+ return BaseResponse.OK(uploadResponseDTO.getFilePath(), uploadResponseDTO.getResult());
+
+// FileOutputStream fos = new FileOutputStream(jsonObject.getString("fileName"));
+// byte[] buffer = new byte[1024];
+// int read;
+// while ((read = uploadStream.read(buffer)) != -1) {
+// fos.write(buffer, 0, read);
+// }
+// return BaseResponse.OK("","");
+ } catch (Exception e) {
+ return BaseResponse.FAIL(Arrays.toString(e.getStackTrace()), e.getMessage());
+ }
+ }
+
+ @NaslLogic
+ public static BaseResponse createPDF(CreateRequest request) {
+
+ try{
+ // 按模块填充内容
+ if(CollectionUtils.isEmpty(request.data)) {
+ return BaseResponse.FAIL("数据为空!");
+ }
+ if(StringUtils.isBlank(request.fileName)){
+ return BaseResponse.FAIL("文件名称不能为空,格式 *.pdf");
+ }
+
+ Document document = new Document();
+ PdfWriter writer = PdfWriter.getInstance(document,new FileOutputStream(request.fileName));
+ document.open();
+
+ // 支持中文字体 TODO:后续考虑通过参数传入字体名称
+ BaseFont chinese = BaseFont.createFont("STSong-Light","UniGB-UCS2-H",BaseFont.NOT_EMBEDDED);
+ Font chineseFont = new Font(chinese);
+
+ for (Map> item : request.data) {
+
+ if(MapUtils.isEmpty(item)){
+ continue;
+ }
+
+ for (Map.Entry> entry : item.entrySet()) {
+
+ if(CollectionUtils.isEmpty(entry.getValue())){
+ continue;
+ }
+
+ switch (entry.getKey()) {
+ case "pdf-table":
+ fillTable(document,entry.getValue(),chineseFont);
+ break;
+ case "pdf-image":
+ fillImage(document,entry.getValue(),chineseFont);
+ break;
+ case "pdf-paragraph":
+ fillParagraph(document,entry.getValue(),chineseFont);
+ break;
+ case "pdf-title":
+ fillTitle(document,entry.getValue(),chineseFont);
+ break;
+ default:
+ return BaseResponse.FAIL("不支持的类型:" + entry.getKey());
+ }
+ }
+ }
+ document.close();
+ writer.close();
+
+ // 上传到oss
+ UploadResponseDTO uploadResponseDTO = fileUtils.uploadFileV2(new File(request.fileName));
+
+ return BaseResponse.OK(uploadResponseDTO.getFilePath(), uploadResponseDTO.getResult());
+ } catch (DocumentException | IOException | InvocationTargetException | NoSuchMethodException |
+ IllegalAccessException e) {
+ return BaseResponse.FAIL(Arrays.toString(e.getStackTrace()),e.getMessage());
+ }
+ }
+
+ private static void fillParagraph(Document document,List data,Font font) throws DocumentException {
+ for (String item : data) {
+ Paragraph paragraph = new Paragraph(new Paragraph(item,font));
+ document.add(paragraph);
+ }
+ }
+
+ private static void fillTitle(Document document,List data,Font font) throws DocumentException {
+ for (String item : data) {
+ Paragraph paragraph = new Paragraph(item,font);
+ paragraph.setAlignment(Element.ALIGN_CENTER);
+ document.add(paragraph);
+ }
+ }
+
+ private static void fillImage(Document document,List data,Font font) throws DocumentException, IOException {
+ for (String item : data) {
+ // 先下载图片
+ File file = FileUtils.downloadFile(item);
+
+ Image image = Image.getInstance(file.getAbsolutePath());
+ image.scaleAbsolute(100,100);
+ document.add(image);
+
+ file.delete();
+ }
+ }
+
+ private static void fillTable(Document document,List data,Font font) throws DocumentException {
+
+ PdfPTable table = null;
+ List rows = null;
+
+ // 表头取第一行数据,每行数据用,分隔
+ // 用户务必控制每行数据的个数统一,空数据同样需要进行占位
+ for (String row : data) {
+ if(StringUtils.isBlank(row)){
+ continue;
+ }
+ String[] splits = StringUtils.split(row, ",");
+ PdfPCell cells[] = new PdfPCell[splits.length];
+ for (int i = 0; i < splits.length; i++) {
+ if("-".equals(splits[i])){
+ cells[i] = new PdfPCell(new Paragraph(" ",font));
+ }else {
+ cells[i] = new PdfPCell(new Paragraph(splits[i], font));
+ }
+ }
+
+ PdfPRow pRow = new PdfPRow(cells);
+
+ if(Objects.isNull(table)){
+ table = new PdfPTable(splits.length);
+ table.setWidthPercentage(100);
+ table.setSpacingBefore(10f);
+ table.setSpacingAfter(10f);
+ }
+ if (Objects.isNull(rows)) {
+ rows = table.getRows();
+ }
+ rows.add(pRow);
+ }
+
+ if(Objects.nonNull(table)){
+ document.add(table);
+ }
+ }
+}
\ No newline at end of file
diff --git a/pdf-generator/src/main/java/com/netease/lowcode/pdf/extension/itextpdf/FooterIEventHandler.java b/pdf-generator/src/main/java/com/netease/lowcode/pdf/extension/itextpdf/FooterIEventHandler.java
new file mode 100644
index 000000000..c8d5262f1
--- /dev/null
+++ b/pdf-generator/src/main/java/com/netease/lowcode/pdf/extension/itextpdf/FooterIEventHandler.java
@@ -0,0 +1,91 @@
+package com.netease.lowcode.pdf.extension.itextpdf;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.itextpdf.kernel.colors.ColorConstants;
+import com.itextpdf.kernel.events.Event;
+import com.itextpdf.kernel.events.IEventHandler;
+import com.itextpdf.kernel.events.PdfDocumentEvent;
+import com.itextpdf.kernel.font.PdfFontFactory;
+import com.itextpdf.kernel.geom.PageSize;
+import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
+import com.itextpdf.layout.properties.TextAlignment;
+
+import java.io.IOException;
+
+public class FooterIEventHandler implements IEventHandler {
+
+ private JSONObject jsonObject;
+
+ public FooterIEventHandler(JSONObject jsonObject){
+ this.jsonObject = jsonObject;
+ }
+
+ @Override
+ public void handleEvent(Event event) {
+ // 判断是否需要页脚
+ if(!jsonObject.containsKey("footer")) {
+ return;
+ }
+
+ JSONObject footer = jsonObject.getJSONObject("footer");
+
+ if(event instanceof PdfDocumentEvent){
+ PdfDocumentEvent documentEvent = (PdfDocumentEvent) event;
+ com.itextpdf.kernel.pdf.PdfDocument document = documentEvent.getDocument();
+ // 获取当前处理的页码
+ int pageNumber = document.getPageNumber(documentEvent.getPage());
+ // 这里已经可以获取所有页数,可在页脚写入
+ int numberOfPages = document.getNumberOfPages();
+
+ // 获取页面尺寸
+ PageSize pageSize = PdfUtils.getPageSize(jsonObject.getString("pageSize"));
+ if (jsonObject.containsKey("rotate") && jsonObject.getBoolean("rotate")) {
+ pageSize = pageSize.rotate();
+ }
+
+ // 创建一个Canvas对象,用于添加水印
+ com.itextpdf.layout.Canvas canvas = new com.itextpdf.layout.Canvas(
+ new PdfCanvas(documentEvent.getPage()),
+ new PageSize(pageSize)
+ );
+
+ if (footer.containsKey("fontColor")) {
+ canvas.setFontColor(PdfUtils.getColor(footer.getString("fontColor")));
+ } else {
+ canvas.setFontColor(ColorConstants.LIGHT_GRAY);
+ }
+ if (footer.containsKey("fontSize")) {
+ canvas.setFontSize(footer.getInteger("fontSize"));
+ } else {
+ canvas.setFontSize(20);
+ }
+ // 为了支持中文
+ try {
+ // 目前统一从全局配置取,暂不考虑水印单独配置字体
+ if (jsonObject.containsKey("font")) {
+ String fontProgram = jsonObject.getJSONObject("font").getString("fontProgram");
+ String encoding = jsonObject.getJSONObject("font").getString("encoding");
+ canvas.setFont(PdfFontFactory.createFont(fontProgram, encoding));
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ float x = 0, y = 0;
+ if (footer.containsKey("marginLeft")) {
+ x = footer.getFloat("marginLeft");
+ }
+ if (footer.containsKey("marginBottom")) {
+ y = footer.getFloat("marginBottom");
+ }
+
+ TextAlignment textAlignment = TextAlignment.LEFT;
+ if (footer.containsKey("textAlignment")) {
+ textAlignment = TextAlignment.valueOf(footer.getString("textAlignment"));
+ }
+
+ canvas.showTextAligned(footer.getString("text"), x, y, textAlignment);
+ canvas.close();
+ }
+ }
+}
diff --git a/pdf-generator/src/main/java/com/netease/lowcode/pdf/extension/itextpdf/HeaderIEventHandler.java b/pdf-generator/src/main/java/com/netease/lowcode/pdf/extension/itextpdf/HeaderIEventHandler.java
new file mode 100644
index 000000000..52556e935
--- /dev/null
+++ b/pdf-generator/src/main/java/com/netease/lowcode/pdf/extension/itextpdf/HeaderIEventHandler.java
@@ -0,0 +1,99 @@
+package com.netease.lowcode.pdf.extension.itextpdf;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.itextpdf.io.image.ImageDataFactory;
+import com.itextpdf.kernel.colors.ColorConstants;
+import com.itextpdf.kernel.events.Event;
+import com.itextpdf.kernel.events.IEventHandler;
+import com.itextpdf.kernel.events.PdfDocumentEvent;
+import com.itextpdf.kernel.font.PdfFontFactory;
+import com.itextpdf.kernel.geom.PageSize;
+import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
+import com.itextpdf.layout.properties.TextAlignment;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+
+public class HeaderIEventHandler implements IEventHandler {
+
+ private JSONObject jsonObject;
+
+ public HeaderIEventHandler(JSONObject jsonObject){
+ this.jsonObject = jsonObject;
+ }
+
+ @Override
+ public void handleEvent(Event event) {
+
+ // 判断是否需要页眉
+ if(!jsonObject.containsKey("header")) {
+ return;
+ }
+
+ JSONObject header = jsonObject.getJSONObject("header");
+
+ if(event instanceof PdfDocumentEvent){
+ PdfDocumentEvent documentEvent = (PdfDocumentEvent) event;
+ com.itextpdf.kernel.pdf.PdfDocument document = documentEvent.getDocument();
+
+ // 获取页面尺寸
+ PageSize pageSize = PdfUtils.getPageSize(jsonObject.getString("pageSize"));
+ if (jsonObject.containsKey("rotate") && jsonObject.getBoolean("rotate")) {
+ pageSize = pageSize.rotate();
+ }
+
+ // 创建一个Canvas对象,用于添加水印
+ com.itextpdf.layout.Canvas canvas = new com.itextpdf.layout.Canvas(
+ new PdfCanvas(documentEvent.getPage()),
+ new PageSize(pageSize)
+ );
+
+ if (header.containsKey("fontColor")) {
+ canvas.setFontColor(PdfUtils.getColor(header.getString("fontColor")));
+ } else {
+ canvas.setFontColor(ColorConstants.LIGHT_GRAY);
+ }
+ if (header.containsKey("fontSize")) {
+ canvas.setFontSize(header.getInteger("fontSize"));
+ } else {
+ canvas.setFontSize(20);
+ }
+ // 为了支持中文
+ try {
+ // 目前统一从全局配置取,暂不考虑水印单独配置字体
+ if (jsonObject.containsKey("font")) {
+ String fontProgram = jsonObject.getJSONObject("font").getString("fontProgram");
+ String encoding = jsonObject.getJSONObject("font").getString("encoding");
+ canvas.setFont(PdfFontFactory.createFont(fontProgram, encoding));
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ float x = 0, y = pageSize.getTop();
+ if (header.containsKey("marginLeft")) {
+ x = header.getFloat("marginLeft");
+ }
+ if (header.containsKey("marginTop")) {
+ y = pageSize.getTop() - header.getFloat("marginTop");
+ }
+
+ TextAlignment textAlignment = TextAlignment.LEFT;
+ if (header.containsKey("textAlignment")) {
+ textAlignment = TextAlignment.valueOf(header.getString("textAlignment"));
+ }
+
+ canvas.showTextAligned(header.getString("text"), x, y, textAlignment);
+
+ // 设置页眉图标
+ if(header.containsKey("image")){
+ JSONObject image = header.getJSONObject("image");
+
+ com.itextpdf.layout.element.Image imageElement = NodeCreator.image(image);
+ canvas.add(imageElement);
+
+ }
+ canvas.close();
+ }
+ }
+}
diff --git a/pdf-generator/src/main/java/com/netease/lowcode/pdf/extension/itextpdf/NodeCreator.java b/pdf-generator/src/main/java/com/netease/lowcode/pdf/extension/itextpdf/NodeCreator.java
new file mode 100644
index 000000000..559d8b45a
--- /dev/null
+++ b/pdf-generator/src/main/java/com/netease/lowcode/pdf/extension/itextpdf/NodeCreator.java
@@ -0,0 +1,342 @@
+package com.netease.lowcode.pdf.extension.itextpdf;
+
+import com.alibaba.fastjson2.JSONArray;
+import com.alibaba.fastjson2.JSONObject;
+import com.itextpdf.forms.fields.properties.CheckBoxType;
+import com.itextpdf.forms.form.element.CheckBox;
+import com.itextpdf.io.image.ImageDataFactory;
+import com.itextpdf.kernel.colors.CalRgb;
+import com.itextpdf.kernel.colors.DeviceRgb;
+import com.itextpdf.kernel.events.PdfDocumentEvent;
+import com.itextpdf.kernel.font.PdfFontFactory;
+import com.itextpdf.kernel.geom.PageSize;
+import com.itextpdf.kernel.pdf.PdfDocument;
+import com.itextpdf.kernel.pdf.PdfWriter;
+import com.itextpdf.kernel.pdf.canvas.draw.SolidLine;
+import com.itextpdf.layout.Document;
+import com.itextpdf.layout.borders.Border;
+import com.itextpdf.layout.borders.SolidBorder;
+import com.itextpdf.layout.element.*;
+import com.itextpdf.layout.properties.TextAlignment;
+import com.itextpdf.layout.properties.UnitValue;
+import com.netease.lowcode.pdf.extension.structures.NodeTypeEnum;
+import org.apache.commons.lang3.SerializationUtils;
+import org.apache.commons.lang3.StringUtils;
+import sun.misc.BASE64Decoder;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.util.List;
+import java.util.Objects;
+
+public class NodeCreator {
+
+ public static ByteArrayOutputStream node(JSONObject jsonObject) throws IOException {
+
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+
+ String fileName = jsonObject.getString("fileName");
+ if (StringUtils.isBlank(fileName) || !fileName.endsWith(".pdf")) {
+ throw new RuntimeException("fileName必须以 *.pdf 结尾");
+ }
+ PdfDocument pdfDocument = new PdfDocument(new PdfWriter(byteArrayOutputStream));
+
+ // 设置纸张大小和方向: 默认纵向,rotate为横向
+ PageSize pageSize = PdfUtils.getPageSize(jsonObject.getString("pageSize"));
+ if (jsonObject.containsKey("rotate") && jsonObject.getBoolean("rotate")) {
+ pageSize = pageSize.rotate();
+ }
+ pdfDocument.setDefaultPageSize(pageSize);
+ // 添加水印,页眉页脚
+ pdfDocument.addEventHandler(PdfDocumentEvent.END_PAGE, new WaterMaskIEventHandler(jsonObject));
+ pdfDocument.addEventHandler(PdfDocumentEvent.END_PAGE, new HeaderIEventHandler(jsonObject));
+ pdfDocument.addEventHandler(PdfDocumentEvent.END_PAGE,new FooterIEventHandler(jsonObject));
+ pdfDocument.addEventHandler(PdfDocumentEvent.END_PAGE,new PageNumberIEventHandler(jsonObject));
+
+ Document document = new Document(pdfDocument);
+
+ JSONObject fontJSONObject = jsonObject.getJSONObject("font");
+ document.setFont(PdfFontFactory.createFont(fontJSONObject.getString("fontProgram"),fontJSONObject.getString("encoding")));
+ if (jsonObject.containsKey("fontSize")) {
+ document.setFontSize(jsonObject.getFloat("fontSize"));
+ } else {
+ document.setFontSize(7);
+ }
+
+ // 设置页边距
+ if(jsonObject.containsKey("marginLeft")){
+ document.setLeftMargin(jsonObject.getFloat("marginLeft"));
+ }
+ if(jsonObject.containsKey("marginRight")){
+ document.setRightMargin(jsonObject.getFloat("marginRight"));
+ }
+ if(jsonObject.containsKey("marginTop")){
+ document.setTopMargin(jsonObject.getFloat("marginTop"));
+ }
+ if(jsonObject.containsKey("marginBottom")){
+ document.setBottomMargin(jsonObject.getFloat("marginBottom"));
+ }
+
+ JSONArray nodes = jsonObject.getJSONArray("nodes");
+
+ if (Objects.isNull(nodes)) {
+ document.close();
+ return byteArrayOutputStream;
+ }
+
+ nodes.toJavaList(JSONObject.class).forEach(nodeObj -> {
+ if ("Image".equalsIgnoreCase(nodeObj.getString("type"))) {
+ // 文档插入图片
+ document.add(image(nodeObj));
+ } else if ("AreaBreak".equalsIgnoreCase(nodeObj.getString("type"))) {
+ // 文档分页
+ document.add(areaBreak());
+ } else {
+ document.add(NodeTypeEnum.valueOf(nodeObj.getString("type")).exec(nodeObj));
+ }
+ });
+
+ document.close();
+ return byteArrayOutputStream;
+ }
+
+ public static Paragraph paragraph(JSONObject jsonObject) {
+ if(Objects.isNull(jsonObject)){
+ return null;
+ }
+ if(!jsonObject.containsKey("type")){
+ return null;
+ }
+ Paragraph paragraph = new Paragraph();
+ if (jsonObject.containsKey("text")) {
+ paragraph.add(jsonObject.getString("text"));
+ } else {
+ paragraph.add("");
+ }
+ if(jsonObject.containsKey("textAlignment")){
+ paragraph.setTextAlignment(TextAlignment.valueOf(jsonObject.getString("textAlignment")));
+ }
+ if(jsonObject.containsKey("fontSize")){
+ paragraph.setFontSize(jsonObject.getInteger("fontSize"));
+ }
+ if (jsonObject.containsKey("bold") && jsonObject.getBoolean("bold")) {
+ paragraph.setBold();
+ }
+ if(jsonObject.containsKey("underline")){
+ paragraph.setUnderline();
+ }
+ if(jsonObject.containsKey("fontColor")){
+ paragraph.setFontColor(PdfUtils.getColor(jsonObject.getString("fontColor")));
+ }
+ // 如果有rgb,将覆盖上面的color
+ if (jsonObject.containsKey("rgb")) {
+ JSONObject rgb = jsonObject.getJSONObject("rgb");
+ paragraph.setFontColor(new DeviceRgb(rgb.getInteger("red"), rgb.getInteger("green"), rgb.getInteger("blue")));
+ }
+
+ if(jsonObject.containsKey("marginLeft")){
+ paragraph.setMarginLeft(jsonObject.getInteger("marginLeft"));
+ }
+ if(jsonObject.containsKey("marginRight")) {
+ paragraph.setMarginRight(jsonObject.getInteger("marginRight"));
+ }
+
+ if (jsonObject.containsKey("elements")) {
+ JSONArray elements = jsonObject.getJSONArray("elements");
+ elements.toJavaList(JSONObject.class).forEach(obj -> {
+ if("Image".equalsIgnoreCase(obj.getString("type"))){
+ paragraph.add(image(obj));
+ }else{
+ paragraph.add(NodeTypeEnum.valueOf(obj.getString("type")).exec(obj));
+ }
+ });
+ }
+
+ return paragraph;
+ }
+
+ public static CheckBox checkBox(JSONObject jsonObject) {
+ if(Objects.isNull(jsonObject)){
+ return null;
+ }
+ if(!jsonObject.containsKey("type")){
+ return null;
+ }
+
+ CheckBox checkBox = new CheckBox(String.valueOf(System.currentTimeMillis()));
+ if (jsonObject.containsKey("checked")) {
+ checkBox.setChecked(jsonObject.getBoolean("checked"));
+ }
+ if (jsonObject.containsKey("checkBoxType")) {
+ checkBox.setCheckBoxType(CheckBoxType.valueOf(jsonObject.getString("checkBoxType")));
+ }
+ if (jsonObject.containsKey("border")) {
+ checkBox.setBorder(new SolidBorder(jsonObject.getJSONObject("border").getInteger("width")));
+ }
+ if (jsonObject.containsKey("size")) {
+ checkBox.setSize(jsonObject.getInteger("size"));
+ }
+ return checkBox;
+ }
+
+ public static Table table(JSONObject jsonObject){
+ if(Objects.isNull(jsonObject)){
+ return null;
+ }
+ Integer width = jsonObject.getInteger("width");
+ // 列的数量
+ Integer columnSize = jsonObject.getInteger("columnSize");
+ JSONArray cellArray = jsonObject.getJSONArray("cells");
+ List cellList = cellArray.toJavaList(JSONObject.class);
+ // 获取分块数量,表格将在水平方向上扩展
+ if (jsonObject.containsKey("chunkSize")) {
+
+ // 进行分块时,cell的填充方向,默认水平顺序填充
+ if(jsonObject.containsKey("chunkVertical")){
+ // 进行竖直顺序填充
+ // TODO: 暂不支持
+ }
+
+ Integer chunkSize = jsonObject.getInteger("chunkSize");
+ columnSize = columnSize * chunkSize;
+
+ // 末尾对齐
+ int cellListSize = cellList.size();
+ if (cellListSize % columnSize != 0) {
+ for (int i = cellListSize % columnSize; i < columnSize; i++) {
+ // 复制上方表格样式
+ JSONObject clone = SerializationUtils.clone(cellList.get(i % jsonObject.getInteger("columnSize")));
+ JSONArray cellElements = clone.getJSONArray("elements");
+ List elementList = cellElements.toJavaList(JSONObject.class);
+ for (JSONObject elementObj : elementList) {
+ String elementType = elementObj.getString("type");
+ if (NodeTypeEnum.Paragraph.equals(NodeTypeEnum.valueOf(elementType))) {
+ elementObj.put("text", "");
+ }
+ }
+ cellList.add(clone);
+ }
+ }
+ }
+
+ Table table = new Table(columnSize);
+ table.setWidth(UnitValue.createPercentValue(width));
+ cellList.forEach(obj -> table.addCell(NodeCreator.cell(obj)));
+ return table;
+ }
+
+ public static Cell cell(JSONObject jsonObject){
+ if(Objects.isNull(jsonObject)){
+ return null;
+ }
+
+ if(!jsonObject.containsKey("width")){
+ throw new RuntimeException("cell缺少width");
+ }
+
+ Cell cell;
+ if (jsonObject.containsKey("rowspan") && jsonObject.containsKey("colspan")) {
+ cell = new Cell(jsonObject.getInteger("rowspan"), jsonObject.getInteger("colspan"));
+ } else {
+ cell = new Cell();
+ }
+ cell.setWidth(UnitValue.createPercentValue(jsonObject.getInteger("width")));
+
+ if (jsonObject.containsKey("textAlignment")) {
+ cell.setTextAlignment(TextAlignment.valueOf(jsonObject.getString("textAlignment")));
+ }
+
+ if (!jsonObject.containsKey("elements")) {
+ return cell;
+ }
+
+ // 去除默认边框
+ if (jsonObject.containsKey("noBorder") && jsonObject.getBoolean("noBorder")) {
+ cell.setBorder(Border.NO_BORDER);
+ }
+
+ // 设置单元格边框
+ if (jsonObject.containsKey("borderBottom")) {
+ cell.setBorderBottom(new SolidBorder(jsonObject.getJSONObject("borderBottom").getInteger("width")));
+ }
+ if (jsonObject.containsKey("borderTop")) {
+ cell.setBorderTop(new SolidBorder(jsonObject.getJSONObject("borderTop").getInteger("width")));
+ }
+ if (jsonObject.containsKey("borderLeft")) {
+ cell.setBorderLeft(new SolidBorder(jsonObject.getJSONObject("borderLeft").getInteger("width")));
+ }
+ if (jsonObject.containsKey("borderRight")) {
+ cell.setBorderRight(new SolidBorder(jsonObject.getJSONObject("borderRight").getInteger("width")));
+ }
+
+ JSONArray elements = jsonObject.getJSONArray("elements");
+ elements.toJavaList(JSONObject.class).forEach(obj -> {
+ if("Image".equalsIgnoreCase(obj.getString("type"))){
+ cell.add(image(obj));
+ }else{
+ cell.add(NodeTypeEnum.valueOf(obj.getString("type")).exec(obj));
+ }
+ });
+
+ return cell;
+ }
+
+ public static LineSeparator lineSeparator(JSONObject jsonObject){
+ SolidLine lineDrawer;
+ if (jsonObject.containsKey("lineWidth")) {
+ lineDrawer = new SolidLine(jsonObject.getFloat("lineWidth"));
+ } else {
+ lineDrawer = new SolidLine();
+ }
+ LineSeparator lineSeparator = new LineSeparator(lineDrawer);
+ if(jsonObject.containsKey("width")){
+ lineSeparator.setWidth(UnitValue.createPercentValue(jsonObject.getInteger("width")));
+ }
+ if(jsonObject.containsKey("marginLeft")){
+ lineSeparator.setMarginLeft(jsonObject.getInteger("marginLeft"));
+ }
+
+ return lineSeparator;
+ }
+
+ public static Image image(JSONObject jsonObject) {
+ try {
+ BASE64Decoder decoder = new BASE64Decoder();
+ if(!jsonObject.containsKey("base64")){
+ throw new RuntimeException("图片缺少base64编码");
+ }
+ String base64 = jsonObject.getString("base64");
+ if (base64.contains("base64,")) {
+ base64 = base64.substring(base64.indexOf("base64,") + 7);
+ }
+ byte[] bytes = decoder.decodeBuffer(base64);
+ Image image = new Image(ImageDataFactory.create(bytes));
+ float fitWidth = 100, fitHeight = 100;
+ if (jsonObject.containsKey("fitWidth")) {
+ fitWidth = jsonObject.getFloat("fitWidth");
+ }
+ if (jsonObject.containsKey("fitHeight")) {
+ fitHeight = jsonObject.getFloat("fitHeight");
+ }
+ image.scaleToFit(fitWidth, fitHeight);
+ if (jsonObject.containsKey("marginLeft")) {
+ image.setMarginLeft(jsonObject.getInteger("marginLeft"));
+ }
+ if (jsonObject.containsKey("marginTop")) {
+ image.setMarginTop(jsonObject.getInteger("marginTop"));
+ }
+ if (jsonObject.containsKey("opacity")) {
+ // 透明度 0完全透明 1完全不透明
+ image.setOpacity(jsonObject.getFloat("opacity"));
+ }
+ return image;
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static AreaBreak areaBreak() {
+ return new AreaBreak();
+ }
+}
diff --git a/pdf-generator/src/main/java/com/netease/lowcode/pdf/extension/itextpdf/PageNumberIEventHandler.java b/pdf-generator/src/main/java/com/netease/lowcode/pdf/extension/itextpdf/PageNumberIEventHandler.java
new file mode 100644
index 000000000..712a814c4
--- /dev/null
+++ b/pdf-generator/src/main/java/com/netease/lowcode/pdf/extension/itextpdf/PageNumberIEventHandler.java
@@ -0,0 +1,109 @@
+package com.netease.lowcode.pdf.extension.itextpdf;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.itextpdf.kernel.colors.ColorConstants;
+import com.itextpdf.kernel.events.Event;
+import com.itextpdf.kernel.events.IEventHandler;
+import com.itextpdf.kernel.events.PdfDocumentEvent;
+import com.itextpdf.kernel.font.PdfFontFactory;
+import com.itextpdf.kernel.geom.PageSize;
+import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
+import com.itextpdf.layout.properties.TextAlignment;
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.IOException;
+
+public class PageNumberIEventHandler implements IEventHandler {
+
+ private JSONObject jsonObject;
+
+ public PageNumberIEventHandler(JSONObject jsonObject){
+ this.jsonObject = jsonObject;
+ }
+
+ @Override
+ public void handleEvent(Event event) {
+ // 判断是否需要页码
+ if(!jsonObject.containsKey("pageNumber")){
+ return;
+ }
+
+ JSONObject pageNumber = jsonObject.getJSONObject("pageNumber");
+
+ if(event instanceof PdfDocumentEvent){
+ PdfDocumentEvent documentEvent = (PdfDocumentEvent) event;
+ com.itextpdf.kernel.pdf.PdfDocument document = documentEvent.getDocument();
+ // 获取当前处理的页码
+ int pageNo = document.getPageNumber(documentEvent.getPage());
+ // 这里已经可以获取所有页数,可在页脚写入
+ int numberOfPages = document.getNumberOfPages();
+
+ // 获取页面尺寸
+ PageSize pageSize = PdfUtils.getPageSize(jsonObject.getString("pageSize"));
+ if (jsonObject.containsKey("rotate") && jsonObject.getBoolean("rotate")) {
+ pageSize = pageSize.rotate();
+ }
+
+ // 创建一个Canvas对象,用于添加水印
+ com.itextpdf.layout.Canvas canvas = new com.itextpdf.layout.Canvas(
+ new PdfCanvas(documentEvent.getPage()),
+ new PageSize(pageSize)
+ );
+
+ if (pageNumber.containsKey("fontColor")) {
+ canvas.setFontColor(PdfUtils.getColor(pageNumber.getString("fontColor")));
+ } else {
+ canvas.setFontColor(ColorConstants.LIGHT_GRAY);
+ }
+ if (pageNumber.containsKey("fontSize")) {
+ canvas.setFontSize(pageNumber.getInteger("fontSize"));
+ } else {
+ canvas.setFontSize(20);
+ }
+ // 为了支持中文
+ try {
+ // 目前统一从全局配置取,暂不考虑水印单独配置字体
+ if (jsonObject.containsKey("font")) {
+ String fontProgram = jsonObject.getJSONObject("font").getString("fontProgram");
+ String encoding = jsonObject.getJSONObject("font").getString("encoding");
+ canvas.setFont(PdfFontFactory.createFont(fontProgram, encoding));
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ float x = pageSize.getWidth(), y = pageSize.getBottom();
+ if (pageNumber.containsKey("marginRight")) {
+ x = pageSize.getWidth() - pageNumber.getFloat("marginRight");
+ }
+ if (pageNumber.containsKey("marginBottom")) {
+ y = pageNumber.getFloat("marginBottom");
+ }
+
+ TextAlignment textAlignment = TextAlignment.RIGHT;
+ if (pageNumber.containsKey("textAlignment")) {
+ textAlignment = TextAlignment.valueOf(pageNumber.getString("textAlignment"));
+ }
+
+ String text = pageNumber.getString("text");
+ String format = text;
+ if(StringUtils.isNotBlank(text)){
+ String[] split = text.split("%s");
+ // 只显示页码
+ if (split.length - 1 == 1) {
+ format = String.format(text, pageNo);
+ }
+ // 显示页码+总页数
+ else if (split.length - 1 == 2) {
+ format = String.format(text, pageNo, numberOfPages);
+ }
+ else {
+ throw new RuntimeException("页码占位符%s数量只能为1或2个");
+ }
+ }
+
+ canvas.showTextAligned(format, x, y, textAlignment);
+ canvas.close();
+ }
+ }
+}
diff --git a/pdf-generator/src/main/java/com/netease/lowcode/pdf/extension/itextpdf/PdfUtils.java b/pdf-generator/src/main/java/com/netease/lowcode/pdf/extension/itextpdf/PdfUtils.java
new file mode 100644
index 000000000..b4107881d
--- /dev/null
+++ b/pdf-generator/src/main/java/com/netease/lowcode/pdf/extension/itextpdf/PdfUtils.java
@@ -0,0 +1,109 @@
+package com.netease.lowcode.pdf.extension.itextpdf;
+
+import com.itextpdf.kernel.colors.Color;
+import com.itextpdf.kernel.colors.ColorConstants;
+import com.itextpdf.kernel.geom.PageSize;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.Objects;
+
+public class PdfUtils {
+
+ public static String readJson(String path) throws IOException {
+ BufferedReader reader = new BufferedReader(new FileReader(path));
+ StringBuilder sb = new StringBuilder();
+ String line;
+ while((line=reader.readLine())!=null){
+ sb.append(line).append("\n");
+ }
+ reader.close();
+ return sb.toString();
+ }
+
+ public static Color getColor(String name){
+ switch (name){
+ case "RED":
+ return ColorConstants.RED;
+ case "BLUE":
+ return ColorConstants.BLUE;
+ case "CYAN":
+ return ColorConstants.CYAN;
+ case "DARK_GRAY":
+ return ColorConstants.DARK_GRAY;
+ case "GRAY":
+ return ColorConstants.GRAY;
+ case "GREEN":
+ return ColorConstants.GREEN;
+ case "LIGHT_GRAY":
+ return ColorConstants.LIGHT_GRAY;
+ case "MAGENTA":
+ return ColorConstants.MAGENTA;
+ case "ORANGE":
+ return ColorConstants.ORANGE;
+ case "PINK":
+ return ColorConstants.PINK;
+ case "WHITE":
+ return ColorConstants.WHITE;
+ case "YELLOW":
+ return ColorConstants.YELLOW;
+ default:
+ return ColorConstants.BLACK;
+ }
+ }
+
+ public static PageSize getPageSize(String size){
+
+ if (Objects.isNull(size)) {
+ return PageSize.A4;
+ }
+
+ switch (size){
+ case "A0":
+ return PageSize.A0;
+ case "A1":
+ return PageSize.A1;
+ case "A2":
+ return PageSize.A2;
+ case "A3":
+ return PageSize.A3;
+ case "A5":
+ return PageSize.A5;
+ case "A6":
+ return PageSize.A6;
+ case "A7":
+ return PageSize.A7;
+ case "A8":
+ return PageSize.A8;
+ case "A9":
+ return PageSize.A9;
+ case "A10":
+ return PageSize.A10;
+ case "B0":
+ return PageSize.B0;
+ case "B1":
+ return PageSize.B1;
+ case "B2":
+ return PageSize.B2;
+ case "B3":
+ return PageSize.B3;
+ case "B4":
+ return PageSize.B4;
+ case "B5":
+ return PageSize.B5;
+ case "B6":
+ return PageSize.B6;
+ case "B7":
+ return PageSize.B7;
+ case "B8":
+ return PageSize.B8;
+ case "B9":
+ return PageSize.B9;
+ case "B10":
+ return PageSize.B10;
+ default:
+ return PageSize.A4;
+ }
+ }
+}
diff --git a/pdf-generator/src/main/java/com/netease/lowcode/pdf/extension/itextpdf/WaterMaskIEventHandler.java b/pdf-generator/src/main/java/com/netease/lowcode/pdf/extension/itextpdf/WaterMaskIEventHandler.java
new file mode 100644
index 000000000..3a152ae92
--- /dev/null
+++ b/pdf-generator/src/main/java/com/netease/lowcode/pdf/extension/itextpdf/WaterMaskIEventHandler.java
@@ -0,0 +1,111 @@
+package com.netease.lowcode.pdf.extension.itextpdf;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.itextpdf.kernel.colors.ColorConstants;
+import com.itextpdf.kernel.events.Event;
+import com.itextpdf.kernel.events.IEventHandler;
+import com.itextpdf.kernel.events.PdfDocumentEvent;
+import com.itextpdf.kernel.font.PdfFontFactory;
+import com.itextpdf.kernel.geom.PageSize;
+import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
+import com.itextpdf.layout.properties.TextAlignment;
+import com.itextpdf.layout.properties.VerticalAlignment;
+
+import java.io.IOException;
+
+public class WaterMaskIEventHandler implements IEventHandler {
+
+ private JSONObject jsonObject;
+
+ public WaterMaskIEventHandler(JSONObject jsonObject){
+ this.jsonObject = jsonObject;
+ }
+
+ @Override
+ public void handleEvent(Event event) {
+
+ // 判断是否需要水印
+ if(!jsonObject.containsKey("waterMask")) {
+ return;
+ }
+ JSONObject waterMask = jsonObject.getJSONObject("waterMask");
+
+ if(event instanceof PdfDocumentEvent){
+ PdfDocumentEvent documentEvent = (PdfDocumentEvent) event;
+ com.itextpdf.kernel.pdf.PdfDocument document = documentEvent.getDocument();
+ // 获取当前处理的页码
+ int pageNumber = document.getPageNumber(documentEvent.getPage());
+
+ // 获取页面尺寸
+ PageSize pageSize = PdfUtils.getPageSize(jsonObject.getString("pageSize"));
+ if (jsonObject.containsKey("rotate") && jsonObject.getBoolean("rotate")) {
+ pageSize = pageSize.rotate();
+ }
+ // 创建一个Canvas对象,用于添加水印
+ com.itextpdf.layout.Canvas canvas = new com.itextpdf.layout.Canvas(
+ new PdfCanvas(documentEvent.getPage()),
+ new PageSize(pageSize)// 需要判断方向
+ );
+
+ if (waterMask.containsKey("fontColor")) {
+ canvas.setFontColor(PdfUtils.getColor(waterMask.getString("fontColor")));
+ } else {
+ canvas.setFontColor(ColorConstants.LIGHT_GRAY);
+ }
+ if (waterMask.containsKey("fontSize")) {
+ canvas.setFontSize(waterMask.getInteger("fontSize"));
+ } else {
+ canvas.setFontSize(20);
+ }
+ // 为了支持中文
+ try {
+ // 目前统一从全局配置取,暂不考虑水印单独配置字体
+ if (jsonObject.containsKey("font")) {
+ String fontProgram = jsonObject.getJSONObject("font").getString("fontProgram");
+ String encoding = jsonObject.getJSONObject("font").getString("encoding");
+ canvas.setFont(PdfFontFactory.createFont(fontProgram, encoding));
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ // 获取水印起始坐标
+ Float x = 0f;
+ if (waterMask.containsKey("x")) {
+ x = waterMask.getFloat("x");
+ }
+ Float y = 0f;
+ if (waterMask.containsKey("y")) {
+ y = waterMask.getFloat("y");
+ }
+ // 获取水印旋转角度 (0~360°,逆时针方向)
+ Double angle = 30.0;
+ if (waterMask.containsKey("angle")) {
+ angle = waterMask.getDouble("angle");
+ }
+
+ float x_space = pageSize.getWidth(), y_space = pageSize.getHeight();
+ if (waterMask.containsKey("xAxisElementSpacing")) {
+ x_space = waterMask.getFloat("xAxisElementSpacing");
+ }
+ if (waterMask.containsKey("yAxisElementSpacing")) {
+ y_space = waterMask.getFloat("yAxisElementSpacing");
+ }
+
+ // 重复多次,调整坐标即可实现重复水印填充满页
+ // x轴方向重复
+ for (float xi = x; xi <= pageSize.getWidth() + x_space; xi += x_space) {
+ // y轴方向重复
+ for (float yi = y; yi <= pageSize.getHeight() + y_space; yi += y_space) {
+ // x 水印的横坐标
+ // y 水印的纵坐标
+ // radAngle 水印倾斜角度,直接输入0~360°即可,会自动转换为弧度
+ canvas.showTextAligned(new com.itextpdf.layout.element.Paragraph(waterMask.getString("text")),
+ xi, yi, pageNumber, TextAlignment.CENTER,
+ VerticalAlignment.TOP, (float) Math.toRadians(angle));
+ }
+ }
+ canvas.close();
+ }
+ }
+}
diff --git a/pdf-generator/src/main/java/com/netease/lowcode/pdf/extension/structures/BaseResponse.java b/pdf-generator/src/main/java/com/netease/lowcode/pdf/extension/structures/BaseResponse.java
new file mode 100644
index 000000000..ba1d51d61
--- /dev/null
+++ b/pdf-generator/src/main/java/com/netease/lowcode/pdf/extension/structures/BaseResponse.java
@@ -0,0 +1,84 @@
+package com.netease.lowcode.pdf.extension.structures;
+
+import com.netease.lowcode.core.annotation.NaslStructure;
+
+@NaslStructure
+public class BaseResponse {
+ public Boolean success;
+ public String msg;
+ public String trace;
+ public String result;
+ public String filePath;
+
+ public static BaseResponse OK(String filePath,String result) {
+ BaseResponse response = new BaseResponse();
+ response.setSuccess(true);
+ response.setMsg("OK");
+ response.setResult(result);
+ response.setFilePath(filePath);
+ return response;
+ }
+
+ public static BaseResponse FAIL(String msg) {
+ return FAIL("",msg);
+ }
+
+ public static BaseResponse FAIL(String trace,String msg) {
+ BaseResponse response = new BaseResponse();
+ response.setSuccess(false);
+ response.setMsg(msg);
+ response.setTrace(trace);
+ return response;
+ }
+
+ public Boolean getSuccess() {
+ return success;
+ }
+
+ public void setSuccess(Boolean success) {
+ this.success = success;
+ }
+
+ public String getMsg() {
+ return msg;
+ }
+
+ public void setMsg(String msg) {
+ this.msg = msg;
+ }
+
+ public String getTrace() {
+ return trace;
+ }
+
+ public void setTrace(String trace) {
+ this.trace = trace;
+ }
+
+ public String getResult() {
+ return result;
+ }
+
+ public void setResult(String result) {
+ this.result = result;
+ }
+
+ public String getFilePath() {
+ return filePath;
+ }
+
+ public void setFilePath(String filePath) {
+ this.filePath = filePath;
+ }
+
+ @Override
+ public String toString() {
+ return "BaseResponse{" +
+ "success=" + success +
+ ", msg='" + msg + '\'' +
+ ", trace='" + trace + '\'' +
+ ", result='" + result + '\'' +
+ ", filePath='" + filePath + '\'' +
+ '}';
+ }
+}
diff --git a/pdf-generator/src/main/java/com/netease/lowcode/pdf/extension/structures/CreateByTemplateRequest.java b/pdf-generator/src/main/java/com/netease/lowcode/pdf/extension/structures/CreateByTemplateRequest.java
new file mode 100644
index 000000000..4619b4f0d
--- /dev/null
+++ b/pdf-generator/src/main/java/com/netease/lowcode/pdf/extension/structures/CreateByTemplateRequest.java
@@ -0,0 +1,35 @@
+package com.netease.lowcode.pdf.extension.structures;
+
+import com.netease.lowcode.core.annotation.NaslStructure;
+
+@NaslStructure
+public class CreateByTemplateRequest {
+
+ public String templateUrl;
+ public String jsonData;
+ public String exportFileName;
+
+ public String getTemplateUrl() {
+ return templateUrl;
+ }
+
+ public void setTemplateUrl(String templateUrl) {
+ this.templateUrl = templateUrl;
+ }
+
+ public String getJsonData() {
+ return jsonData;
+ }
+
+ public void setJsonData(String jsonData) {
+ this.jsonData = jsonData;
+ }
+
+ public String getExportFileName() {
+ return exportFileName;
+ }
+
+ public void setExportFileName(String exportFileName) {
+ this.exportFileName = exportFileName;
+ }
+}
diff --git a/pdf-generator/src/main/java/com/netease/lowcode/pdf/extension/structures/CreateByXlsxRequest.java b/pdf-generator/src/main/java/com/netease/lowcode/pdf/extension/structures/CreateByXlsxRequest.java
new file mode 100644
index 000000000..b5a2f4fac
--- /dev/null
+++ b/pdf-generator/src/main/java/com/netease/lowcode/pdf/extension/structures/CreateByXlsxRequest.java
@@ -0,0 +1,54 @@
+package com.netease.lowcode.pdf.extension.structures;
+
+import com.netease.lowcode.core.annotation.NaslStructure;
+
+@NaslStructure
+public class CreateByXlsxRequest {
+ public String templateUrl;
+ public String jsonData;
+ public String exportFileName;
+ // 纸张大小,默认A4
+ public String pageSize = "A4";
+ // 纸张方向,默认纵向
+ public Boolean rotate = false;
+
+ public Boolean getRotate() {
+ return rotate;
+ }
+
+ public void setRotate(Boolean rotate) {
+ this.rotate = rotate;
+ }
+
+ public String getPageSize() {
+ return pageSize;
+ }
+
+ public void setPageSize(String pageSize) {
+ this.pageSize = pageSize;
+ }
+
+ public String getTemplateUrl() {
+ return templateUrl;
+ }
+
+ public void setTemplateUrl(String templateUrl) {
+ this.templateUrl = templateUrl;
+ }
+
+ public String getJsonData() {
+ return jsonData;
+ }
+
+ public void setJsonData(String jsonData) {
+ this.jsonData = jsonData;
+ }
+
+ public String getExportFileName() {
+ return exportFileName;
+ }
+
+ public void setExportFileName(String exportFileName) {
+ this.exportFileName = exportFileName;
+ }
+}
diff --git a/pdf-generator/src/main/java/com/netease/lowcode/pdf/extension/structures/CreateRequest.java b/pdf-generator/src/main/java/com/netease/lowcode/pdf/extension/structures/CreateRequest.java
new file mode 100644
index 000000000..00c7c8bab
--- /dev/null
+++ b/pdf-generator/src/main/java/com/netease/lowcode/pdf/extension/structures/CreateRequest.java
@@ -0,0 +1,16 @@
+package com.netease.lowcode.pdf.extension.structures;
+
+import com.netease.lowcode.core.annotation.NaslStructure;
+
+import java.util.List;
+import java.util.Map;
+
+@NaslStructure
+public class CreateRequest {
+ public String fileName;
+ /**
+ * key表示数据类型: 文本、图片、表格
+ * value表示具体内容,如果是表格类型,value是包括表头在内的数据集合,
+ */
+ public List