diff --git a/EasyExcel/pom.xml b/EasyExcel/pom.xml index 6a52c4b08..d8230d98b 100644 --- a/EasyExcel/pom.xml +++ b/EasyExcel/pom.xml @@ -6,7 +6,7 @@ com.netease.lowcode.extensions EasyExcel - 2.3.1 + 2.4.1 8 @@ -23,13 +23,13 @@ nasl-metadata-collector com.netease.lowcode - 0.8.0 + 0.14.1 com.alibaba easyexcel - 3.3.2 + 3.3.3 @@ -115,10 +115,10 @@ com.netease.lowcode nasl-metadata-maven-plugin - 1.4.3 + 1.7.1 false - + true diff --git a/EasyExcel/src/main/java/com/netease/lowcode/extensions/EasyExcelTools.java b/EasyExcel/src/main/java/com/netease/lowcode/extensions/EasyExcelTools.java index 37b2d378c..f60302bc7 100644 --- a/EasyExcel/src/main/java/com/netease/lowcode/extensions/EasyExcelTools.java +++ b/EasyExcel/src/main/java/com/netease/lowcode/extensions/EasyExcelTools.java @@ -156,16 +156,19 @@ public static InputStream openUrlStream(String url) { * @return */ private static String getFileUrl(String url) { - int index = url.indexOf("/upload/"); - HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); - int port = request.getLocalPort(); - if (port == -1) { - return url; - } - if (index >= 0) { - url = "http://localhost:" + port + "/upload/" + url.substring(index + "/upload/".length()); + try { + int index = url.indexOf("/upload/"); + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + int port = request.getLocalPort(); + if (port == -1) { + return url; + } + if (index >= 0) { + url = "http://localhost:" + port + "/upload/" + url.substring(index + "/upload/".length()); + } + } catch (Exception e) { + log.error("url转本地地址异常", e); } - return url; } diff --git a/itextpdf-tool/README.md b/itextpdf-tool/README.md new file mode 100644 index 000000000..143861436 --- /dev/null +++ b/itextpdf-tool/README.md @@ -0,0 +1,370 @@ +# 依赖库名称 + +封装ITextPdf工具,提供通用生成pdf文件的功能。 + +## 逻辑说明: + +### createDocument 创建文档 + +返回的文档key须保存,后续用来生成文档/压缩包。 + +``` + /** + * 创建pdf文档 + * + * @param fileName 文件名称(带扩展名) + * @return 文档key + * @throws IOException + * @throws DocumentException + */ + @NaslLogic + public String createDocument(String fileName) +``` + +### closeDocument 关闭文档 + +关闭文档后,在服务器本地生成文件,返回文件本地路径。该本地路径跟文件连接器无关。 + +``` +/** + * 关闭文档 + * + * @param documentKey + * @return 文件本地路径 + */ + @NaslLogic + public String closeDocument(String documentKey) { + +``` + +### closeDocumentGetFileURl 关闭文档并且获取文件地址 + +关闭文档后,在服务器本地生成文件,并且把文件通过默认文件连接器配置上传至文件连接器。最终返回文件连接器中该文件的地址。 + +``` + /** + * 关闭文档并且获取文件地址 + * + * @param documentKey + * @return 文件访问信息 + */ + @NaslLogic + public UploadResponseDTO closeDocumentGetFileURl(String documentKey) + +``` + +### addDocumentElement 组装元素 + +组装元素,返回组装结果。 + +``` + /** + * 组装元素 + * + * @param addedElementKey 子元素key + * @param addElementKey 父元素key + * @return + * @throws DocumentException + */ + @NaslLogic + public Boolean addDocumentElement(String addedElementKey, String addElementKey) { + +``` + +### buildParagraph 构造段落 + +构造段落,返回段落key。 + +``` + /** + * 构造段落 + * + * @param paragraphStr 段落文本。可空,空时创建空段落 + * @param iTextParagraphStructure 段落配置 + * @param iTextFontStructure 段落文本格式。paragraphStr为空时可空 + * @return 段落key + * @throws DocumentException + * @throws IOException + */ + @NaslLogic + public String buildParagraph(String paragraphStr, ITextParagraphStructure iTextParagraphStructure, ITextFontStructure iTextFontStructure) +``` + +### buildTable 构造表格 + +构造表格,返回表格key。 + +``` + /** + * 构造表格 + * + * @param iTextTableStructure 表格配置 + * @return + * @throws DocumentException + */ + @NaslLogic + public String buildTable(ITextTableStructure iTextTableStructure) +``` + +### buildCell 构造单元格 + +构造单元格,返回单元格key。 + +``` +/** + * @param cellStr 单元格文本。可空,空时创建空段落 + * @param iTextCellStructure 单元格配置 + * @param iTextFontStructure 单元格文本格式。paragraphStr为空时可空 + * @return + * @throws DocumentException + * @throws IOException + */ + @NaslLogic + public String buildCell(String cellStr, ITextCellStructure iTextCellStructure, ITextFontStructure iTextFontStructure) { + +``` + +### buildChunk 构造Chunk + +构造Chunk,返回Chunk key。 + +``` + /** + * 构造chunk + * + * @param chunkStr 文本。可空,空时创建空段落 + * @param iTextChunkStructure 配置 + * @param iTextFontStructure 文本格式 + * @return + */ + @NaslLogic + public String buildChunk(String chunkStr, ITextChunkStructure iTextChunkStructure, ITextFontStructure iTextFontStructure) { + +``` + +### buildPhrase 构造短语 + +构造短语,返回短语key。 + +``` + /** + * 构造短语 + * @param phraseStr 文本。可空,空时创建空段落 + * @param iTextFontStructure 文本格式 + * @param iTextPhraseStructure 短语配置 + * @return + */ + @NaslLogic + public String buildPhrase(String phraseStr, ITextFontStructure iTextFontStructure, ITextPhraseStructure iTextPhraseStructure) { + +``` + +### buildListItem 构造列表项 + +构造列表项,返回列表项key。 + +``` + /** + * 构造列表 + * @param listItemStr 列表项文本。可空,空时创建空列表 + * @param iTextFontStructure 列表项文本格式 + * @param iTextListStructure 列表配置 + * @return + */ + @NaslLogic + public String buildListItem(String listItemStr, ITextFontStructure iTextFontStructure, ITextListStructure iTextListStructure) { + +``` + +### buildImage 构造图片 + +构造图片,返回图片key。 + +``` + /** + * 构造图片元素 + * @param imageUrl 图片url + * @param iTextImageStructure 图片配置 + * @return + */ + @NaslLogic + public String buildImage(String imageUrl, ITextImageStructure iTextImageStructure) { + +``` + +### buildImageBase64 使用base64字符串构造图片 + +构造图片,返回图片key。 + +``` +/** + +* 构造图片元素 +* +* @param base64ImageString 图片base64字符串 +* @param iTextImageStructure 图片配置 +* @return + */ + @NaslLogic + public String buildImageBase64(String base64ImageString, ITextImageStructure iTextImageStructure) { +``` + +### addCanvasImage 新增图片canvas + +新增图片canvas,置于顶层。可用于插入盖章等全文档随意位置的图片元素。 + +``` +/** + * 新增图片canvas,置于顶层 + * @param documentKey + * @param imageKey + * @return + */ + @NaslLogic + public Boolean addCanvasImage(String documentKey, String imageKey) +``` +### mergePdf 合并pdf +``` + /** + * 合并pdf + * + * @param inputFiles 原文件地址集合 + * @param targetFileName 目标文件名 + * @param isDeleteOriginLocalFile 是否删除本地原文件 + * @return 目标文件本地路径 + */ + @NaslLogic + public String mergePdf(List inputFiles, String targetFileName, Boolean isDeleteOriginLocalFile) +``` +### +``` mergePdfToNosFileURl 合并pdf,并上传到nos + /** + * 合并pdf,并上传到nos + * + * @param inputFiles 原文件地址集合 + * @param targetFileName 目标文件名,文件名称不可重复 + * @param isDeleteOriginLocalFile 是否删除本地原文件 + * @return nos文件访问信息 + */ + @NaslLogic + public UploadResponseDTO mergePdfToNosFileURl(List inputFiles, String targetFileName, Boolean isDeleteOriginLocalFile) +``` + +## 结构说明 + +### 字体格式 + +| 字段名 | 类型 | 描述/可选值 | +|:------------:|:-------:|:---------------------------------------------------------------------------------------- +| fontName | String | 字体名称,默认STSong-Light +| fontEncoding | String | 字体编码,默认UniGB-UCS2-H +| embedded | Boolean | 是否嵌入字体:true表示将字体文件嵌入文档(确保跨设备显示一致),false表示不嵌入 +| forceRead | Boolean | 强制读取字体:true表示忽略系统缓存强制重新加载字体文件(解决字体更新问题) +| size | Double | 字体大小(单位:磅/pt) +| style | String | 字体样式:NORMAL/BOLD/ITALIC/UNDERLINE/STRIKETHRU/BOLDITALIC/UNDEFINED/DEFAULTSIZE +| color | String | 颜色:WHITE/LIGHT_GRAY/GRAY/DARK_GRAY/BLACK/RED/PINK/ORANGE/YELLOW/GREEN/MAGENTA/CYAN/BLUE + +### 单元格配置 + +| 字段名 | 类型 | 描述/可选值 | +|:-------------------------|:----------|:--------------------------------------------------------------------------------------------------------------------------------------------------| +| border | String | 全局边框类型:UNDEFINED/TOP/BOTTOM/LEFT/RIGHT/NO_BORDER/BOX | +| borderWidth | Double | 单元格边框宽度 | +| padding | Double | 单元格内边距 | +| borderColor | String | 边框颜色:WHITE/LIGHT_GRAY/GRAY/DARK_GRAY/BLACK/RED/PINK/ORANGE/YELLOW/GREEN/MAGENTA/CYAN/BLUE | +| paddingLeft | Double | 左边距 | +| paddingRight | Double | 右边距 | +| paddingTop | Double | 上边距 | +| paddingBottom | Double | 下边距 | +| horizontalAlignmentText | String | 水平对齐方式:ALIGN_UNDEFINED/ALIGN_LEFT/ALIGN_CENTER/ALIGN_RIGHT/ALIGN_JUSTIFIED/ALIGN_TOP/ALIGN_MIDDLE/ALIGN_BOTTOM/ALIGN_BASELINE/ALIGN_JUSTIFIED_ALL +| verticalAlignmentText | String | 垂直对齐方式:ALIGN_UNDEFINED/ALIGN_LEFT/ALIGN_CENTER/ALIGN_RIGHT/ALIGN_JUSTIFIED/ALIGN_TOP/ALIGN_MIDDLE/ALIGN_BOTTOM/ALIGN_BASELINE/ALIGN_JUSTIFIED_ALL +| fixedHeight | Double | 固定高度 +| minimumHeight | Double | 最小高度 +| fixedLeading | Double | 行间距(固定值) +| multipliedLeading | Double | 行间距(倍数) +| backgroundColor | String | 背景颜色:WHITE/LIGHT_GRAY/GRAY/DARK_GRAY/BLACK/RED/PINK/ORANGE/YELLOW/GREEN/MAGENTA/CYAN/BLUE +| colSpan | Integer | 横向合并列数 +| rowSpan | Integer | 纵向合并行数 + +### Chunk配置 + +| 字段名 | 类型 | 描述/可选值 | +|:-----------------|:--------|:---------------------------------------------------------------------------------------------------| +| characterSpacing | Double | 设置字符间距 +| anchor | String | 添加超链接(可以是URL或文档内锚点) +| wordSpacing | Double | 设置单词间距(仅对空格字符有效) +| backgroundColor | String | 设置背景色(颜色值或枚举:WHITE/LIGHT_GRAY/GRAY/DARK_GRAY/BLACK/RED/PINK/ORANGE/YELLOW/GREEN/MAGENTA/CYAN/BLUE) + +### 图片配置 + +| 字段名 | 类型 | 描述/可选值 | +|:---------------------|:----------|:------------------------------------------------------------------------------------------------------------------------------------------------| +| scaleToFitHeight | Double | 按比例缩放图片到目标高度(保持宽高比) +| scaleToFitWidth | Double | 按比例缩放图片到目标宽度(保持宽高比) +| scalePercent | Double | 缩放图片的百分比(100表示原尺寸) +| scaleAbsoluteWidth | Double | 固定宽度,高度按比例调整 +| scaleAbsoluteHeight | Double | 固定高度,宽度按比例调整 +| alignText | String | 对齐方式:ALIGN_UNDEFINED/ALIGN_LEFT/ALIGN_CENTER/ALIGN_RIGHT/ALIGN_JUSTIFIED/ALIGN_TOP/ALIGN_MIDDLE/ALIGN_BOTTOM/ALIGN_BASELINE/ALIGN_JUSTIFIED_ALL +| absoluteX | Double | 设置绝对X坐标(相对于页面左下角) +| absoluteY | Double | 设置绝对Y坐标(相对于页面左下角) +| indentationLeft | Double | 左缩进(需在非绝对定位模式下使用) +| border | String | 全局边框:UNDEFINED/TOP/BOTTOM/LEFT/RIGHT/NO_BORDER/BOX +| borderWidth | Double | 边框宽度 +| borderColor | String | 边框颜色:WHITE/LIGHT_GRAY/GRAY/DARK_GRAY/BLACK/RED/PINK/ORANGE/YELLOW/GREEN/MAGENTA/CYAN/BLUE +| spacingBefore | Double | 图片上方的空白间距 +| spacingAfter | Double | 图片下方的空白间距 +| transparency | List | 透明度(ARGB值),示例:[200, 255, 255, 255](A=200, R=255, G=255, B=255) +| anchor | String | 设置超链接(点击跳转URL) +| compressionLevel | Integer | JPEG压缩质量(0-9,0=最低压缩/质量最差,9=最高压缩/质量最好) +| dpiX | Integer | 水平DPI(影响打印质量) +| dpiY | Integer | 垂直DPI(影响打印质量) + +### 列表配置 + +| 字段名 | 类型 | 描述/可选值 | +|:-------------------|:---------|:-------------------------------------------------------------------------------------------------------------------------------------------------| +| alignText | String | 对齐方式:ALIGN_UNDEFINED/ALIGN_LEFT/ALIGN_CENTER/ALIGN_RIGHT/ALIGN_JUSTIFIED/ALIGN_TOP/ALIGN_MIDDLE/ALIGN_BOTTOM/ALIGN_BASELINE/ALIGN_JUSTIFIED_ALL +| indentationLeft | Double | 左缩进(单位:像素) +| indentationRight | Double | 右缩进(单位:像素) +| firstLineIndent | Double | 首行缩进(单位:像素) +| keepTogether | Boolean | 是否禁止分页时拆分段落(true表示整段必须在同一页) +| spacingBefore | Double | 段前间距(单位:像素) +| spacingAfter | Double | 段后间距(单位:像素) +| paddingTop | Double | 上边距(单位:像素) +| fixedLeading | Double | 行间距(固定值,单位:像素) +| multipliedLeading | Double | 行间距(倍数,如1.5表示1.5倍行距) + +### 段落配置 + +| 字段名 | 类型 | 描述/可选值 | +|:------------------|:----------|:-------------------------------------------------------------------------------------------------------------------------------------------------| +| alignText | String | 对齐方式:ALIGN_UNDEFINED/ALIGN_LEFT/ALIGN_CENTER/ALIGN_RIGHT/ALIGN_JUSTIFIED/ALIGN_TOP/ALIGN_MIDDLE/ALIGN_BOTTOM/ALIGN_BASELINE/ALIGN_JUSTIFIED_ALL +| indentationLeft | Double | 左缩进(单位:像素) +| indentationRight | Double | 右缩进(单位:像素) +| firstLineIndent | Double | 首行缩进(单位:像素) +| spacingBefore | Double | 段前间距(单位:像素) +| spacingAfter | Double | 段后间距(单位:像素) +| keepTogether | Boolean | 是否禁止分页时拆分段落(true表示整段必须在同一页) +| fixedLeading | Double | 行间距(固定值,单位:像素) +| multipliedLeading | Double | 行间距(倍数,如 1.5 表示1.5倍行距) + +### Phrase短语配置 + +| 字段名 | 类型 | 描述/可选值 | +|:-------------------|:--------|:-------------------------------| +| fixedLeading | Double | 行间距(固定值,单位:像素/磅) +| multipliedLeading | Double | 行间距(倍数,基于字体大小计算,如1.5表示1.5倍行距) + +### 表格配置 + +| 字段名 | 类型 | 描述/可选值 | +|:------------------|:----------|:--------------------------------------------------------------------------------------------------------------------------------------------------| +| alignText | String | 文本对齐方式:ALIGN_UNDEFINED/ALIGN_LEFT/ALIGN_CENTER/ALIGN_RIGHT/ALIGN_JUSTIFIED/ALIGN_TOP/ALIGN_MIDDLE/ALIGN_BOTTOM/ALIGN_BASELINE/ALIGN_JUSTIFIED_ALL +| indentationLeft | Double | 左缩进(单位:像素) +| indentationRight | Double | 右缩进(单位:像素) +| firstLineIndent | Double | 首行缩进(单位:像素) +| spacingBefore | Double | 段前间距(单位:像素) +| spacingAfter | Double | 段后间距(单位:像素) +| keepTogether | Boolean | 段落分页控制:true-禁止拆分段落(整段必须在同一页)false-允许拆分段落 +| fixedLeading | Double | 固定行高(单位:像素) +| multipliedLeading | Double | 相对行高(基于字体大小的倍数,如1.5=1.5倍行距) diff --git a/itextpdf-tool/jar/nasl-metadata-collector-0.12.0/nasl-metadata-collector-0.12.0/install.bat b/itextpdf-tool/jar/nasl-metadata-collector-0.12.0/nasl-metadata-collector-0.12.0/install.bat new file mode 100644 index 000000000..f1647dff0 --- /dev/null +++ b/itextpdf-tool/jar/nasl-metadata-collector-0.12.0/nasl-metadata-collector-0.12.0/install.bat @@ -0,0 +1 @@ +mvn install:install-file -Dfile="nasl-metadata-collector-0.12.0.jar" -DgroupId="com.netease.lowcode" -DartifactId="nasl-metadata-collector" -Dversion="0.12.0" -Dpackaging="jar" -DgeneratePom=true \ No newline at end of file diff --git a/itextpdf-tool/jar/nasl-metadata-collector-0.12.0/nasl-metadata-collector-0.12.0/install.sh b/itextpdf-tool/jar/nasl-metadata-collector-0.12.0/nasl-metadata-collector-0.12.0/install.sh new file mode 100644 index 000000000..f1647dff0 --- /dev/null +++ b/itextpdf-tool/jar/nasl-metadata-collector-0.12.0/nasl-metadata-collector-0.12.0/install.sh @@ -0,0 +1 @@ +mvn install:install-file -Dfile="nasl-metadata-collector-0.12.0.jar" -DgroupId="com.netease.lowcode" -DartifactId="nasl-metadata-collector" -Dversion="0.12.0" -Dpackaging="jar" -DgeneratePom=true \ No newline at end of file diff --git a/itextpdf-tool/jar/nasl-metadata-collector-0.12.0/nasl-metadata-collector-0.12.0/nasl-metadata-collector-0.12.0.jar b/itextpdf-tool/jar/nasl-metadata-collector-0.12.0/nasl-metadata-collector-0.12.0/nasl-metadata-collector-0.12.0.jar new file mode 100644 index 000000000..675e25054 Binary files /dev/null and b/itextpdf-tool/jar/nasl-metadata-collector-0.12.0/nasl-metadata-collector-0.12.0/nasl-metadata-collector-0.12.0.jar differ diff --git a/itextpdf-tool/jar/nasl-metadata-maven-plugin-1.5.1/install.bat b/itextpdf-tool/jar/nasl-metadata-maven-plugin-1.5.1/install.bat new file mode 100644 index 000000000..900bd9c25 --- /dev/null +++ b/itextpdf-tool/jar/nasl-metadata-maven-plugin-1.5.1/install.bat @@ -0,0 +1 @@ +mvn install:install-file -Dfile="nasl-metadata-maven-plugin-1.5.1.jar" -DpomFile="nasl-metadata-maven-plugin-1.5.1.pom" \ No newline at end of file diff --git a/itextpdf-tool/jar/nasl-metadata-maven-plugin-1.5.1/install.sh b/itextpdf-tool/jar/nasl-metadata-maven-plugin-1.5.1/install.sh new file mode 100644 index 000000000..900bd9c25 --- /dev/null +++ b/itextpdf-tool/jar/nasl-metadata-maven-plugin-1.5.1/install.sh @@ -0,0 +1 @@ +mvn install:install-file -Dfile="nasl-metadata-maven-plugin-1.5.1.jar" -DpomFile="nasl-metadata-maven-plugin-1.5.1.pom" \ No newline at end of file diff --git a/itextpdf-tool/jar/nasl-metadata-maven-plugin-1.5.1/nasl-metadata-maven-plugin-1.5.1.jar b/itextpdf-tool/jar/nasl-metadata-maven-plugin-1.5.1/nasl-metadata-maven-plugin-1.5.1.jar new file mode 100644 index 000000000..d584d03c3 Binary files /dev/null and b/itextpdf-tool/jar/nasl-metadata-maven-plugin-1.5.1/nasl-metadata-maven-plugin-1.5.1.jar differ diff --git a/itextpdf-tool/jar/nasl-metadata-maven-plugin-1.5.1/nasl-metadata-maven-plugin-1.5.1.pom b/itextpdf-tool/jar/nasl-metadata-maven-plugin-1.5.1/nasl-metadata-maven-plugin-1.5.1.pom new file mode 100644 index 000000000..be041b53f --- /dev/null +++ b/itextpdf-tool/jar/nasl-metadata-maven-plugin-1.5.1/nasl-metadata-maven-plugin-1.5.1.pom @@ -0,0 +1,223 @@ + + 4.0.0 + + com.netease.lowcode + nasl-metadata-maven-plugin + 1.5.1 + maven-plugin + + Nasl Metadata Maven Plugin + + UTF-8 + 3.2.5 + 9.4.54.v20240208 + 3.3.0 + 1.0.2.v20150114 + 8 + 1.7.36 + 4.9.2 + + + + + + org.apache.maven + maven-artifact + ${mavenVersion} + provided + + + org.apache.maven + maven-plugin-api + ${mavenVersion} + provided + + + org.apache.maven + maven-model + ${mavenVersion} + provided + + + org.apache.maven + maven-core + ${mavenVersion} + provided + + + org.apache.maven + maven-repository-metadata + ${mavenVersion} + provided + + + org.apache.maven + maven-settings + ${mavenVersion} + provided + + + org.apache.maven + maven-aether-provider + ${mavenVersion} + provided + + + + + org.apache.maven.doxia + doxia-sink-api + 1.12.0 + + + org.codehaus.plexus + plexus-container-default + + + + + org.apache.maven.reporting + maven-reporting-api + 3.1.1 + + + org.apache.maven.reporting + maven-reporting-impl + 3.2.0 + + + org.codehaus.plexus + plexus-container-default + + + + + + + org.codehaus.plexus + plexus-archiver + ${plexus-archiver.version} + + + org.codehaus.plexus + plexus-utils + 4.0.0 + + + org.codehaus.plexus + plexus-xml + 3.0.0 + + + org.codehaus.plexus + plexus-io + 3.4.2 + + + org.codehaus.plexus + plexus-i18n + 1.0-beta-10 + + + org.codehaus.plexus + plexus-component-api + + + + + + + org.apache.maven.shared + maven-dependency-analyzer + 1.13.2 + + + org.apache.maven.shared + maven-dependency-tree + 3.2.1 + + + org.apache.maven.shared + maven-common-artifact-filters + 3.3.2 + + + org.apache.maven.shared + maven-artifact-transfer + 0.13.1 + + + org.apache.maven.shared + maven-shared-utils + 3.4.2 + + + + + org.apache.maven.plugin-tools + maven-plugin-annotations + provided + 3.11.0 + + + javax.inject + javax.inject + 1 + + + + org.eclipse.aether + aether-api + ${resolverVersion} + provided + + + org.eclipse.aether + aether-util + ${resolverVersion} + provided + + + org.sonatype.plexus + plexus-build-api + 0.0.7 + compile + + + + + commons-io + commons-io + 2.16.1 + + + + com.fasterxml.jackson.core + jackson-databind + 2.15.3 + + + + + + + org.apache.maven.plugins + maven-plugin-plugin + 3.5 + + nasl-metadata-maven-plugin + true + + + + org.apache.maven.plugins + maven-compiler-plugin + + 8 + 8 + + + + + diff --git a/itextpdf-tool/pom.xml b/itextpdf-tool/pom.xml new file mode 100644 index 000000000..f79958800 --- /dev/null +++ b/itextpdf-tool/pom.xml @@ -0,0 +1,94 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.2.9.RELEASE + + + + com.netease.lowcode + itextpdf-tool + 1.2.0 + + + 8 + 8 + UTF-8 + 3.11 + + + + + nasl-metadata-collector + com.netease.lowcode + 0.12.0 + + + + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + org.slf4j + slf4j-api + provided + + + + org.springframework.boot + spring-boot-starter-web + provided + + + org.springframework.boot + spring-boot-starter-test + test + + + com.itextpdf + itextpdf + 5.5.13.3 + + + com.itextpdf + itext-asian + 5.2.0 + + + com.netease.cloud + codewave-file-connector-library + 1.0.3 + provided + + + + + + com.netease.lowcode + nasl-metadata-maven-plugin + 1.5.1 + + false + true + + + + + archive + + + + + + + \ No newline at end of file diff --git a/itextpdf-tool/src/main/java/com/netease/lowcode/lib/ItextpdfToolBasicSpringEnvironmentConfiguration.java b/itextpdf-tool/src/main/java/com/netease/lowcode/lib/ItextpdfToolBasicSpringEnvironmentConfiguration.java new file mode 100644 index 000000000..f3d487b8a --- /dev/null +++ b/itextpdf-tool/src/main/java/com/netease/lowcode/lib/ItextpdfToolBasicSpringEnvironmentConfiguration.java @@ -0,0 +1,12 @@ +package com.netease.lowcode.lib; + +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +/** + * 加入spring环境配置(在spring.factories中指定) + */ +@Configuration +@ComponentScan(basePackageClasses = LibraryAutoScan.class) +public class ItextpdfToolBasicSpringEnvironmentConfiguration { +} diff --git a/itextpdf-tool/src/main/java/com/netease/lowcode/lib/LibraryAutoScan.java b/itextpdf-tool/src/main/java/com/netease/lowcode/lib/LibraryAutoScan.java new file mode 100644 index 000000000..b573d47af --- /dev/null +++ b/itextpdf-tool/src/main/java/com/netease/lowcode/lib/LibraryAutoScan.java @@ -0,0 +1,8 @@ +package com.netease.lowcode.lib; + +/** + * 依赖库自动扫描类 + * @author system + */ +public class LibraryAutoScan { +} diff --git a/itextpdf-tool/src/main/java/com/netease/lowcode/lib/api/ItextPdfApi.java b/itextpdf-tool/src/main/java/com/netease/lowcode/lib/api/ItextPdfApi.java new file mode 100644 index 000000000..3fdf14829 --- /dev/null +++ b/itextpdf-tool/src/main/java/com/netease/lowcode/lib/api/ItextPdfApi.java @@ -0,0 +1,662 @@ +package com.netease.lowcode.lib.api; + + +import com.itextpdf.text.*; +import com.itextpdf.text.pdf.*; +import com.netease.lowcode.core.annotation.NaslLogic; +import com.netease.lowcode.lib.file.FileConnectorUtils; +import com.netease.lowcode.lib.file.UploadResponseDTO; +import com.netease.lowcode.lib.itextpdf.BaseFontBuilder; +import com.netease.lowcode.lib.itextpdf.PdfBuilderUtil; +import com.netease.lowcode.lib.itextpdf.PdfDocumentBuilder; +import com.netease.lowcode.lib.structure.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import javax.annotation.Resource; +import java.io.*; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; +import java.util.*; +import java.util.stream.Collectors; + +@Component +public class ItextPdfApi { + + //参数使用LCAP_EXTENSION_LOGGER后日志会显示在平台日志功能中 + private static final Logger log = LoggerFactory.getLogger("LCAP_EXTENSION_LOGGER"); + + private String tempDirectory = "./itextpdf"; + private Map pdfDocumentBuilderMap = new HashMap<>(); + private Map elementMap = new HashMap<>(); + @Resource + private FileConnectorUtils fileConnectorUtils; + + /** + * 合并pdf,并上传到nos + * + * @param inputFiles 原文件地址集合 + * @param targetFileName 目标文件名,文件名称不可重复 + * @param isDeleteOriginLocalFile 是否删除本地原文件 + * @return nos文件访问信息 + */ + @NaslLogic + public UploadResponseDTO mergePdfToNosFileURl(List inputFiles, String targetFileName, Boolean isDeleteOriginLocalFile) { + String localFilePath = mergePdf(inputFiles, targetFileName, isDeleteOriginLocalFile); + String[] path = localFilePath.split(File.separator); + String fileName = path[path.length - 1]; + try { + FileInputStream fis = new FileInputStream(localFilePath); + UploadResponseDTO res = fileConnectorUtils.fileUploadV2(fis, fileName, new HashMap<>()); + return res; + } catch (Exception e) { + log.error("close document error", e); + throw new IllegalArgumentException("close document error"); + } finally { + try { + //删除本地文件 + Files.delete(Paths.get(localFilePath)); + String directoryPath = localFilePath.replace(fileName, ""); + Files.delete(Paths.get(directoryPath)); + } catch (IOException e) { + log.error("删除失败", e); + } + } + } + + /** + * 获取inputFiles的pdf文件,并下载到本地 + * + * @param inputFiles 原文件地址集合 + * @return inputFiles的pdf文件 + */ + private List getInputFilesLocal(List inputFiles) throws IOException { + //inputFiles去重 + inputFiles = inputFiles.stream().distinct().collect(Collectors.toList()); + List inputFilesFinal = new ArrayList<>(); + String key = "document-" + UUID.randomUUID(); + //创建目录 + String tempDirectoryFinal = tempDirectory + File.separator + key + File.separator; + File file = new File(tempDirectoryFinal); + file.mkdirs(); + for (String input : inputFiles) { + String targetFileName = "tomergepdf-" + UUID.randomUUID() + ".pdf"; + String targetFileNamePath = tempDirectoryFinal + targetFileName; + if (input.startsWith("http://") || input.startsWith("https://")) { + InputStream inputStream = fileConnectorUtils.fileDownloadV2(input); + // 使用try-with-resources确保流关闭 + try (OutputStream outputStream = new FileOutputStream(targetFileNamePath)) { + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + } + inputFilesFinal.add(targetFileNamePath); + } else { + inputFilesFinal.add(input); + } + } + return inputFilesFinal; + } + + /** + * 合并pdf + * + * @param inputFiles 原文件地址集合 + * @param targetFileName 目标文件名 + * @param isDeleteOriginLocalFile 是否删除本地原文件 + * @return 目标文件本地路径 + */ + @NaslLogic + public String mergePdf(List inputFiles, String targetFileName, Boolean isDeleteOriginLocalFile) { + String key = "merge-document-" + UUID.randomUUID(); + //创建目录 + String tempDirectoryFinal = tempDirectory + File.separator + key + File.separator; + String targetFileNamePath = tempDirectoryFinal + targetFileName; + File file = new File(tempDirectoryFinal); + file.mkdirs(); + Document document = new Document(); + PdfCopy copy; + List readers = new ArrayList<>(); + List inputFilesFinal = new ArrayList<>(); + try { + inputFilesFinal = getInputFilesLocal(inputFiles); + copy = new PdfSmartCopy(document, Files.newOutputStream(Paths.get(targetFileNamePath))); + document.open(); + for (String input : inputFilesFinal) { + PdfReader reader = new PdfReader(new RandomAccessFileOrArray(input), null); + readers.add(reader); + copy.addDocument(reader); + } + return targetFileNamePath; + } catch (Exception e) { + log.error("合并失败: ", e); + throw new IllegalArgumentException("合并失败"); + } finally { + try { + Set directorys = new HashSet<>(); + if (isDeleteOriginLocalFile) { + //删除本地合并前原文件 + for (String input : inputFilesFinal) { + Files.delete(Paths.get(input)); + String directory = input.replace(input.substring(input.lastIndexOf(File.separator)), ""); + directorys.add(directory); + } + for (String directory : directorys) { + Files.delete(Paths.get(directory)); + } + } + if (document.isOpen()) { + document.close(); + } + for (PdfReader r : readers) { + if (r != null) { + r.close(); + } + } + } catch (Exception e) { + log.error(" ", e); + } + } + } + + /** + * 创建pdf文档 + * + * @param fileName 文件名称(带扩展名) + * @return 文档key + * @throws IOException + * @throws DocumentException + */ + @NaslLogic + public String createDocument(String fileName) { + try { + String key = "document-" + UUID.randomUUID(); + //创建目录 + String tempDirectoryFinal = tempDirectory + File.separator + key + File.separator; + File file = new File(tempDirectoryFinal); + file.mkdirs(); + //将pdf档写入响应输出流 + Document document = new Document(); + PdfWriter pdfWriter = PdfWriter.getInstance(document, Files.newOutputStream(Paths.get(tempDirectoryFinal + fileName))); + document.open(); + //信息放入内存 + PdfDocumentBuilder pdfDocumentBuilder = new PdfDocumentBuilder(document, tempDirectoryFinal, fileName, pdfWriter); + pdfDocumentBuilderMap.put(key, pdfDocumentBuilder); + return key; + } catch (Exception e) { + log.error("create document error", e); + throw new IllegalArgumentException("create document error"); + } + } + + /** + * 关闭文档 + * + * @param documentKey + * @return 文件本地路径 + */ + @NaslLogic + public String closeDocument(String documentKey) { + try { + PdfDocumentBuilder pdfDocumentBuilder = pdfDocumentBuilderMap.get(documentKey); + if (pdfDocumentBuilder == null) { + return null; + } + pdfDocumentBuilder.getDocument().close(); + String path = pdfDocumentBuilder.getTempDirectoryFinal() + pdfDocumentBuilder.getFileName(); + pdfDocumentBuilderMap.remove(documentKey); + return path; + } catch (Exception e) { + log.error("close document error", e); + throw new IllegalArgumentException("close document error"); + } + } + + /** + * 关闭文档并且获取文件地址 + * + * @param documentKey + * @return 文件访问信息 + */ + @NaslLogic + public UploadResponseDTO closeDocumentGetFileURl(String documentKey) { + PdfDocumentBuilder pdfDocumentBuilder = pdfDocumentBuilderMap.get(documentKey); + if (pdfDocumentBuilder == null) { + return null; + } + try { + pdfDocumentBuilder.getDocument().close(); + FileInputStream fis = new FileInputStream(pdfDocumentBuilder.getTempDirectoryFinal() + File.separator + pdfDocumentBuilder.getFileName()); + UploadResponseDTO res = fileConnectorUtils.fileUploadV2(fis, pdfDocumentBuilder.getFileName(), new HashMap<>()); + pdfDocumentBuilderMap.remove(documentKey); + return res; + } catch (Exception e) { + log.error("close document error", e); + throw new IllegalArgumentException("close document error"); + } finally { + try { + Files.delete(Paths.get(pdfDocumentBuilder.getTempDirectoryFinal() + File.separator + pdfDocumentBuilder.getFileName())); + Files.delete(Paths.get(pdfDocumentBuilder.getTempDirectoryFinal())); + } catch (IOException e) { + log.error("删除失败", e); + } + } + } + + /** + * 构造段落 + * + * @param paragraphStr 段落文本。可空,空时创建空段落 + * @param iTextParagraphStructure 段落配置 + * @param iTextFontStructure 段落文本格式。paragraphStr为空时可空 + * @return 段落key + * @throws DocumentException + * @throws IOException + */ + @NaslLogic + public String buildParagraph(String paragraphStr, ITextParagraphStructure iTextParagraphStructure, ITextFontStructure iTextFontStructure) { + try { + Paragraph paragraph = new Paragraph(); + if (!StringUtils.isEmpty(paragraphStr)) { + Font font = BaseFontBuilder.setFont(iTextFontStructure); + paragraph = new Paragraph(paragraphStr, font); + } + PdfBuilderUtil.fillParagraph(paragraph, iTextParagraphStructure); + String paragraphKey = "paragraph-" + UUID.randomUUID(); + elementMap.put(paragraphKey, paragraph); + return paragraphKey; + } catch (Exception e) { + log.error("build paragraph error", e); + throw new IllegalArgumentException("build paragraph error"); + } + } + + /** + * 构造表格 + * + * @param iTextTableStructure 表格配置 + * @return + * @throws DocumentException + */ + @NaslLogic + public String buildTable(ITextTableStructure iTextTableStructure) { + try { + PdfPTable table = new PdfPTable(iTextTableStructure.getNumColumns()); + PdfBuilderUtil.fillTable(table, iTextTableStructure); + String tableKey = "table-" + UUID.randomUUID(); + elementMap.put(tableKey, table); + return tableKey; + } catch (Exception e) { + log.error("build table error", e); + throw new IllegalArgumentException("build table error"); + } + } + + /** + * @param cellStr 单元格文本。可空,空时创建空段落 + * @param iTextCellStructure 单元格配置 + * @param iTextFontStructure 单元格文本格式。paragraphStr为空时可空 + * @return + * @throws DocumentException + * @throws IOException + */ + @NaslLogic + public String buildCell(String cellStr, ITextCellStructure iTextCellStructure, ITextFontStructure iTextFontStructure) { + try { + PdfPCell cell = new PdfPCell(); + if (!StringUtils.isEmpty(cellStr)) { + Font font = BaseFontBuilder.setFont(iTextFontStructure); + cell = new PdfPCell(new Paragraph(cellStr, font)); + } + PdfBuilderUtil.fillCell(cell, iTextCellStructure); + String cellKey = "cell-" + UUID.randomUUID(); + elementMap.put(cellKey, cell); + return cellKey; + } catch (Exception e) { + log.error("build cell error", e); + throw new IllegalArgumentException("build cell error"); + } + } + + /** + * 构造chunk + * + * @param chunkStr 文本。可空,空时创建空段落 + * @param iTextChunkStructure 配置 + * @param iTextFontStructure 文本格式 + * @return + */ + @NaslLogic + public String buildChunk(String chunkStr, ITextChunkStructure iTextChunkStructure, ITextFontStructure iTextFontStructure) { + try { + Chunk chunk; + if (iTextFontStructure == null) { + chunk = new Chunk(chunkStr); + } else { + Font font = BaseFontBuilder.setFont(iTextFontStructure); + chunk = new Chunk(chunkStr, font); + PdfBuilderUtil.fillChunk(chunk, iTextChunkStructure); + } + String chunkKey = "chunk-" + UUID.randomUUID(); + elementMap.put(chunkKey, chunk); + return chunkKey; + } catch (Exception e) { + log.error("build chunk error", e); + throw new IllegalArgumentException("build chunk error"); + } + } + + /** + * 构造短语 + * + * @param phraseStr 文本。可空,空时创建空段落 + * @param iTextFontStructure 文本格式 + * @param iTextPhraseStructure 短语配置 + * @return + */ + @NaslLogic + public String buildPhrase(String phraseStr, ITextFontStructure iTextFontStructure, ITextPhraseStructure iTextPhraseStructure) { + try { + Phrase phrase = new Phrase(); + if (!StringUtils.isEmpty(phraseStr)) { + phrase = new Phrase(phraseStr, BaseFontBuilder.setFont(iTextFontStructure)); + } + PdfBuilderUtil.fillPhrase(phrase, iTextPhraseStructure); + String phraseKey = "phrase-" + UUID.randomUUID(); + elementMap.put(phraseKey, phrase); + return phraseKey; + } catch (Exception e) { + log.error("build phrase error", e); + throw new IllegalArgumentException("build phrase error"); + } + } + + /** + * 新增图片canvas,置于顶层 + * + * @param documentKey + * @param imageKey + * @return + */ + @NaslLogic + public Boolean addCanvasImage(String documentKey, String imageKey) { + try { + PdfDocumentBuilder pdfDocumentBuilder = pdfDocumentBuilderMap.get(documentKey); + if (pdfDocumentBuilder == null) { + throw new IllegalArgumentException("documentKey is not exist"); + } + Image image = (Image) elementMap.get(imageKey); + if (image == null) { + throw new IllegalArgumentException("imageKey is not exist"); + } + PdfWriter pdfWriter = pdfDocumentBuilder.getPdfWriter(); + PdfContentByte canvas = pdfWriter.getDirectContent(); + canvas.addImage(image); + return true; + } catch (Exception e) { + log.error("add canvasImage error", e); + throw new IllegalArgumentException("add canvasImage error"); + } + } + + /** + * 构造列表 + * + * @param listItemStr 列表项文本。可空,空时创建空列表 + * @param iTextFontStructure 列表项文本格式 + * @param iTextListStructure 列表配置 + * @return + */ + @NaslLogic + public String buildListItem(String listItemStr, ITextFontStructure iTextFontStructure, ITextListStructure iTextListStructure) { + try { + ListItem listItem = new ListItem(); + if (!StringUtils.isEmpty(listItemStr)) { + listItem = new ListItem(listItemStr, BaseFontBuilder.setFont(iTextFontStructure)); + } + PdfBuilderUtil.filListItem(listItem, iTextListStructure); + String listItemKey = "listItem-" + UUID.randomUUID(); + elementMap.put(listItemKey, listItem); + return listItemKey; + } catch (Exception e) { + log.error("build listItem error", e); + throw new IllegalArgumentException("build listItem error"); + } + } + + /** + * 构造图片元素 + * + * @param imageUrl 图片url + * @param iTextImageStructure 图片配置 + * @return + */ + @NaslLogic + public String buildImage(String imageUrl, ITextImageStructure iTextImageStructure) { + try { + Image image = Image.getInstance(new URL(imageUrl)); + PdfBuilderUtil.fillImage(image, iTextImageStructure); + String imageKey = "image-" + UUID.randomUUID(); + elementMap.put(imageKey, image); + return imageKey; + } catch (Exception e) { + log.error("build image error", e); + throw new IllegalArgumentException("build image error"); + } + } + + /** + * 使用base64字符串构造图片 + * + * @param base64ImageString 图片base64字符串 + * @param iTextImageStructure 图片配置 + * @return + */ + @NaslLogic + public String buildImageBase64(String base64ImageString, ITextImageStructure iTextImageStructure) { + try { + byte[] imageBytes = Base64.getDecoder().decode(base64ImageString); + Image image = Image.getInstance(imageBytes); + PdfBuilderUtil.fillImage(image, iTextImageStructure); + String imageKey = "image-" + UUID.randomUUID(); + elementMap.put(imageKey, image); + return imageKey; + } catch (Exception e) { + log.error("build image error", e); + throw new IllegalArgumentException("build image error"); + } + } + + /** + * 组装元素 + * + * @param addedElementKey 子元素key + * @param addElementKey 父元素key + * @return + * @throws DocumentException + */ + @NaslLogic + public Boolean addDocumentElement(String addedElementKey, String addElementKey) { + boolean flag = true; + try { + PdfDocumentBuilder pdfDocumentBuilder = pdfDocumentBuilderMap.get(addElementKey); + Object elementSon = elementMap.get(addedElementKey); + if (pdfDocumentBuilder != null) { + if (elementSon instanceof Paragraph) { + Paragraph paragraph = (Paragraph) elementSon; + Document document = pdfDocumentBuilder.getDocument(); + document.add(paragraph); + pdfDocumentBuilder.setDocument(document); + } else if (elementSon instanceof Phrase) { + Phrase phrase = (Phrase) elementSon; + Document document = pdfDocumentBuilder.getDocument(); + document.add(phrase); + pdfDocumentBuilder.setDocument(document); + } else if (elementSon instanceof PdfPTable) { + PdfPTable pdfPTable = (PdfPTable) elementSon; + Document document = pdfDocumentBuilder.getDocument(); + document.add(pdfPTable); + pdfDocumentBuilder.setDocument(document); + } else if (elementSon instanceof Image) { + Image image = (Image) elementSon; + Document document = pdfDocumentBuilder.getDocument(); + document.add(image); + pdfDocumentBuilder.setDocument(document); + } else if (elementSon instanceof ListItem) { + ListItem listItem = (ListItem) elementSon; + Document document = pdfDocumentBuilder.getDocument(); + document.add(listItem); + pdfDocumentBuilder.setDocument(document); + } else if (elementSon instanceof Chunk) { + Chunk chunk = (Chunk) elementSon; + Document document = pdfDocumentBuilder.getDocument(); + document.add(chunk); + pdfDocumentBuilder.setDocument(document); + } else { + log.error("addedElementKey获取的elementSon类型错误"); + flag = false; + } + } else { + Object elementParent = elementMap.get(addElementKey); + if (elementParent instanceof Paragraph) { + Paragraph paragraph = (Paragraph) elementParent; + if (elementSon instanceof Paragraph) { + Paragraph paragraphSon = (Paragraph) elementSon; + paragraph.add(paragraphSon); + elementMap.put(addElementKey, paragraph); + } else if (elementSon instanceof Phrase) { + Phrase phrase = (Phrase) elementSon; + paragraph.add(phrase); + elementMap.put(addElementKey, paragraph); + } else if (elementSon instanceof PdfPTable) { + PdfPTable pdfPTable = (PdfPTable) elementSon; + paragraph.add(pdfPTable); + elementMap.put(addElementKey, paragraph); + } else if (elementSon instanceof Image) { + Image image = (Image) elementSon; + paragraph.add(image); + elementMap.put(addElementKey, paragraph); + } else if (elementSon instanceof ListItem) { + ListItem listItem = (ListItem) elementSon; + paragraph.add(listItem); + elementMap.put(addElementKey, paragraph); + } else if (elementSon instanceof Chunk) { + Chunk chunk = (Chunk) elementSon; + paragraph.add(chunk); + elementMap.put(addElementKey, paragraph); + } else { + flag = false; + log.error("addedElementKey获取的elementSon类型错误"); + } + } else if (elementParent instanceof PdfPTable) { + PdfPTable pdfPTable = (PdfPTable) elementParent; + if (elementSon instanceof Paragraph) { + Paragraph paragraphSon = (Paragraph) elementSon; + pdfPTable.addCell(paragraphSon); + elementMap.put(addElementKey, pdfPTable); + } else if (elementSon instanceof Phrase) { + Phrase phrase = (Phrase) elementSon; + pdfPTable.addCell(phrase); + elementMap.put(addElementKey, pdfPTable); + } else if (elementSon instanceof PdfPTable) { + PdfPTable pdfPTableSon = (PdfPTable) elementSon; + pdfPTable.addCell(pdfPTableSon); + elementMap.put(addElementKey, pdfPTable); + } else if (elementSon instanceof Image) { + Image image = (Image) elementSon; + pdfPTable.addCell(image); + elementMap.put(addElementKey, pdfPTable); + } else if (elementSon instanceof ListItem) { + ListItem listItem = (ListItem) elementSon; + pdfPTable.addCell(listItem); + elementMap.put(addElementKey, pdfPTable); + } else if (elementSon instanceof PdfPCell) { + PdfPCell cell = (PdfPCell) elementSon; + pdfPTable.addCell(cell); + elementMap.put(addElementKey, pdfPTable); + } else { + flag = false; + log.error("addedElementKey获取的elementSon类型错误"); + } + } else if (elementParent instanceof PdfPCell) { + PdfPCell pdfPCell = (PdfPCell) elementParent; + if (elementSon instanceof Paragraph) { + Paragraph paragraphSon = (Paragraph) elementSon; + pdfPCell.addElement(paragraphSon); + elementMap.put(addElementKey, pdfPCell); + } else if (elementSon instanceof Phrase) { + Phrase phrase = (Phrase) elementSon; + pdfPCell.addElement(phrase); + elementMap.put(addElementKey, pdfPCell); + } else if (elementSon instanceof PdfPTable) { + PdfPTable pdfPTable = (PdfPTable) elementSon; + pdfPCell.addElement(pdfPTable); + elementMap.put(addElementKey, pdfPCell); + } else if (elementSon instanceof Image) { + Image image = (Image) elementSon; + pdfPCell.addElement(image); + elementMap.put(addElementKey, pdfPCell); + } else if (elementSon instanceof ListItem) { + ListItem listItem = (ListItem) elementSon; + pdfPCell.addElement(listItem); + elementMap.put(addElementKey, pdfPCell); + } else if (elementSon instanceof Chunk) { + Chunk chunk = (Chunk) elementSon; + pdfPCell.addElement(chunk); + elementMap.put(addElementKey, pdfPCell); + } else { + flag = false; + log.error("addedElementKey获取的elementSon类型错误"); + } + } else if (elementParent instanceof ListItem) { + ListItem listItem = (ListItem) elementParent; + if (elementSon instanceof Paragraph) { + Paragraph paragraphSon = (Paragraph) elementSon; + listItem.add(paragraphSon); + elementMap.put(addElementKey, listItem); + } else if (elementSon instanceof Phrase) { + Phrase phrase = (Phrase) elementSon; + listItem.add(phrase); + elementMap.put(addElementKey, listItem); + } else if (elementSon instanceof PdfPTable) { + PdfPTable pdfPTable = (PdfPTable) elementSon; + listItem.add(pdfPTable); + elementMap.put(addElementKey, listItem); + } else if (elementSon instanceof Image) { + Image image = (Image) elementSon; + listItem.add(image); + elementMap.put(addElementKey, listItem); + } else if (elementSon instanceof ListItem) { + ListItem listItemSon = (ListItem) elementSon; + listItem.add(listItemSon); + elementMap.put(addElementKey, listItem); + } else if (elementSon instanceof Chunk) { + Chunk chunk = (Chunk) elementSon; + listItem.add(chunk); + elementMap.put(addElementKey, listItem); + } else { + flag = false; + log.error("addedElementKey获取的elementSon类型错误"); + } + } else { + flag = false; + log.error("addElementKey获取的elementParent类型错误"); + } + } + } catch (Exception e) { + log.error("build addElement error", e); + throw new IllegalArgumentException("build addElement error"); + } + if (flag) { + elementMap.remove(addedElementKey); + } + return flag; + } + +} diff --git a/itextpdf-tool/src/main/java/com/netease/lowcode/lib/enums/BaseColorEnum.java b/itextpdf-tool/src/main/java/com/netease/lowcode/lib/enums/BaseColorEnum.java new file mode 100644 index 000000000..f45beabaa --- /dev/null +++ b/itextpdf-tool/src/main/java/com/netease/lowcode/lib/enums/BaseColorEnum.java @@ -0,0 +1,60 @@ +package com.netease.lowcode.lib.enums; + +import com.itextpdf.text.BaseColor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public enum BaseColorEnum { + WHITE("WHITE", BaseColor.WHITE), + LIGHT_GRAY("LIGHT_GRAY", BaseColor.LIGHT_GRAY), + GRAY("GRAY", BaseColor.GRAY), + DARK_GRAY("DARK_GRAY", BaseColor.DARK_GRAY), + BLACK("BLACK", BaseColor.BLACK), + RED("RED", BaseColor.RED), + PINK("PINK", BaseColor.PINK), + ORANGE("ORANGE", BaseColor.ORANGE), + YELLOW("YELLOW", BaseColor.YELLOW), + GREEN("GREEN", BaseColor.GREEN), + MAGENTA("MAGENTA", BaseColor.MAGENTA), + CYAN("CYAN", BaseColor.CYAN), + BLUE("BLUE", BaseColor.BLUE), + ; + private static final Logger log = LoggerFactory.getLogger("LCAP_EXTENSION_LOGGER"); + + private String color; + private BaseColor baseColor; + + BaseColorEnum(String color, BaseColor baseColor) { + this.color = color; + this.baseColor = baseColor; + } + + /** + * 根据color获取BaseColor + */ + public static BaseColor getBaseColor(String color) { + for (BaseColorEnum value : BaseColorEnum.values()) { + if (value.color.equals(color)) { + return value.baseColor; + } + } + log.error("未定义的颜色color:{}", color); + return BaseColor.BLACK; + } + + public String getColor() { + return color; + } + + public void setColor(String color) { + this.color = color; + } + + public BaseColor getBaseColor() { + return baseColor; + } + + public void setBaseColor(BaseColor baseColor) { + this.baseColor = baseColor; + } +} diff --git a/itextpdf-tool/src/main/java/com/netease/lowcode/lib/enums/ElementAlignEnum.java b/itextpdf-tool/src/main/java/com/netease/lowcode/lib/enums/ElementAlignEnum.java new file mode 100644 index 000000000..d6eb48ad9 --- /dev/null +++ b/itextpdf-tool/src/main/java/com/netease/lowcode/lib/enums/ElementAlignEnum.java @@ -0,0 +1,39 @@ +package com.netease.lowcode.lib.enums; + +import com.itextpdf.text.Element; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public enum ElementAlignEnum { + ALIGN_UNDEFINED("ALIGN_UNDEFINED", Element.ALIGN_UNDEFINED), + ALIGN_LEFT("ALIGN_LEFT", Element.ALIGN_LEFT), + ALIGN_CENTER("ALIGN_CENTER", Element.ALIGN_CENTER), + ALIGN_RIGHT("ALIGN_RIGHT", Element.ALIGN_RIGHT), + ALIGN_JUSTIFIED("ALIGN_JUSTIFIED", Element.ALIGN_JUSTIFIED), + ALIGN_TOP("ALIGN_TOP", Element.ALIGN_TOP), + ALIGN_MIDDLE("ALIGN_MIDDLE", Element.ALIGN_MIDDLE), + ALIGN_BOTTOM("ALIGN_BOTTOM", Element.ALIGN_BOTTOM), + ALIGN_BASELINE("ALIGN_BASELINE", Element.ALIGN_BASELINE), + ALIGN_JUSTIFIED_ALL("ALIGN_JUSTIFIED_ALL", Element.ALIGN_JUSTIFIED_ALL), + ; + private static final Logger log = LoggerFactory.getLogger("LCAP_EXTENSION_LOGGER"); + + private String alignText; + private Integer alignElement; + + ElementAlignEnum(String alignText, Integer alignElement) { + this.alignText = alignText; + this.alignElement = alignElement; + } + + public static Integer getAlignElement(String alignText) { + for (ElementAlignEnum elementAlignEnum : ElementAlignEnum.values()) { + if (elementAlignEnum.alignText.equals(alignText)) { + return elementAlignEnum.alignElement; + } + } + log.error("未定义的样式alignText:{}", alignText); + return Element.ALIGN_UNDEFINED; + } + +} diff --git a/itextpdf-tool/src/main/java/com/netease/lowcode/lib/enums/FontStyleEnum.java b/itextpdf-tool/src/main/java/com/netease/lowcode/lib/enums/FontStyleEnum.java new file mode 100644 index 000000000..87045f8a7 --- /dev/null +++ b/itextpdf-tool/src/main/java/com/netease/lowcode/lib/enums/FontStyleEnum.java @@ -0,0 +1,56 @@ +package com.netease.lowcode.lib.enums; + +import com.itextpdf.text.Font; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public enum FontStyleEnum { + NORMAL("NORMAL", Font.NORMAL), + BOLD("BOLD", Font.BOLD), + ITALIC("ITALIC", Font.ITALIC), + UNDERLINE("UNDERLINE", Font.UNDERLINE), + STRIKETHRU("STRIKETHRU", Font.STRIKETHRU), + BOLDITALIC("BOLDITALIC", Font.BOLDITALIC), + UNDEFINED("UNDEFINED", Font.UNDEFINED), + DEFAULTSIZE("DEFAULTSIZE", Font.DEFAULTSIZE), + ; + private static final Logger log = LoggerFactory.getLogger("LCAP_EXTENSION_LOGGER"); + private String style; + private Integer fontStyle; + + FontStyleEnum(String style, Integer fontStyle) { + this.style = style; + this.fontStyle = fontStyle; + } + + /** + * 根据style查询fontStyle + * + * @param style + */ + public static Integer getFontStyle(String style) { + for (FontStyleEnum fontStyleEnum : FontStyleEnum.values()) { + if (fontStyleEnum.getStyle().equals(style)) { + return fontStyleEnum.getFontStyle(); + } + } + log.error("未定义的字体style:{}", style); + return Font.NORMAL; + } + + public String getStyle() { + return style; + } + + public void setStyle(String style) { + this.style = style; + } + + public Integer getFontStyle() { + return fontStyle; + } + + public void setFontStyle(Integer fontStyle) { + this.fontStyle = fontStyle; + } +} diff --git a/itextpdf-tool/src/main/java/com/netease/lowcode/lib/enums/RectangleEnum.java b/itextpdf-tool/src/main/java/com/netease/lowcode/lib/enums/RectangleEnum.java new file mode 100644 index 000000000..12d0f79eb --- /dev/null +++ b/itextpdf-tool/src/main/java/com/netease/lowcode/lib/enums/RectangleEnum.java @@ -0,0 +1,54 @@ +package com.netease.lowcode.lib.enums; + +import com.itextpdf.text.Rectangle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public enum RectangleEnum { + UNDEFINED("UNDEFINED", Rectangle.UNDEFINED), + TOP("TOP", Rectangle.TOP), + BOTTOM("BOTTOM", Rectangle.BOTTOM), + LEFT("LEFT", Rectangle.LEFT), + RIGHT("RIGHT", Rectangle.RIGHT), + NO_BORDER("NO_BORDER", Rectangle.NO_BORDER), + BOX("BOX", Rectangle.BOX);; + private static final Logger log = LoggerFactory.getLogger("LCAP_EXTENSION_LOGGER"); + private String rectangleText; + private Integer rectangle; + + RectangleEnum(String rectangleText, Integer rectangle) { + this.rectangleText = rectangleText; + this.rectangle = rectangle; + } + + /** + * 根据rectangleText获取rectangle + * + * @return rectangle + */ + public static Integer getRectangle(String rectangleText) { + for (RectangleEnum rectangleEnum : RectangleEnum.values()) { + if (rectangleEnum.getRectangleText().equals(rectangleText)) { + return rectangleEnum.getRectangle(); + } + } + log.error("未定义的布局rectangleText:{}", rectangleText); + return Rectangle.UNDEFINED; + } + + public String getRectangleText() { + return rectangleText; + } + + public void setRectangleText(String rectangleText) { + this.rectangleText = rectangleText; + } + + public Integer getRectangle() { + return rectangle; + } + + public void setRectangle(Integer rectangle) { + this.rectangle = rectangle; + } +} diff --git a/itextpdf-tool/src/main/java/com/netease/lowcode/lib/file/Base64Util.java b/itextpdf-tool/src/main/java/com/netease/lowcode/lib/file/Base64Util.java new file mode 100644 index 000000000..0a304eff6 --- /dev/null +++ b/itextpdf-tool/src/main/java/com/netease/lowcode/lib/file/Base64Util.java @@ -0,0 +1,27 @@ +package com.netease.lowcode.lib.file; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.*; +import java.util.Base64; + +public class Base64Util { + private final static Logger log = LoggerFactory.getLogger("LCAP_EXTENSION_LOGGER"); + + public static String convertImageToBase64String(InputStream is) { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int len; + while ((len = is.read(buffer)) != -1) { + baos.write(buffer, 0, len); + } + byte[] imageBytes = baos.toByteArray(); + return Base64.getEncoder().encodeToString(imageBytes); + } catch (IOException e) { + log.error("convertImageToBase64String error", e); + return null; + } + } +} diff --git a/itextpdf-tool/src/main/java/com/netease/lowcode/lib/file/FileConnectorUtils.java b/itextpdf-tool/src/main/java/com/netease/lowcode/lib/file/FileConnectorUtils.java new file mode 100644 index 000000000..a635171e1 --- /dev/null +++ b/itextpdf-tool/src/main/java/com/netease/lowcode/lib/file/FileConnectorUtils.java @@ -0,0 +1,89 @@ +package com.netease.lowcode.lib.file; + +import com.netease.cloud.codewave.file.connector.AbstractFileConnector; +import com.netease.cloud.codewave.file.connector.CodeWaveFileConstants; +import com.netease.cloud.codewave.file.connector.FileConnectionManager; +import com.netease.cloud.codewave.file.connector.FileDownloadResult; +import com.netease.cloud.codewave.file.connector.utils.CodeWaveFileUrl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.http.HttpServletRequest; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +@Component("itextpdfFileConnectorUtils") +public class FileConnectorUtils { + public static final String UPLOAD_API_PATH_PREFIX = "/upload"; + private final static Logger log = LoggerFactory.getLogger("LCAP_EXTENSION_LOGGER"); + + /** + * 文件上传-311及之后版本 + * + * @param fis 文件输入流,不能为空 + * @param fileName 文件名称,包含文件后缀,不能为空 + * @return success true时上传成功,false时上传失败 + */ + public UploadResponseDTO fileUploadV2(InputStream fis, String fileName, Map payloads) { + UploadResponseDTO responseDTO = new UploadResponseDTO(); + // 获取文件连接器 + AbstractFileConnector defaultConnector = FileConnectionManager.getDefaultFileConnector(); + CodeWaveFileUrl fileUrl = new CodeWaveFileUrl(fileName); + //指定默认文件连接器的唯一标志。用在url拼接上。 + fileUrl.addQueryString(CodeWaveFileConstants.FILE_CONNECTION, defaultConnector.fileStorageCode()); + //调用制品配置的文件连接器的上传方法 + CodeWaveFileUrl result = defaultConnector.upload(fis, fileUrl, payloads); + String filePath = ((result.toUrl().startsWith("/")) ? result.toUrl() : "/" + result.toUrl()); + //处理文件url + responseDTO.setFilePath("/upload" + filePath); + ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (Objects.nonNull(requestAttributes)) { + HttpServletRequest request = requestAttributes.getRequest(); + responseDTO.setResult(request.getScheme() + "://" + request.getServerName() + + (80 == request.getServerPort() ? "" : ":" + request.getServerPort()) + + responseDTO.getFilePath()); + } else { + responseDTO.setResult(responseDTO.getFilePath()); + } + responseDTO.setSuccess(true); + responseDTO.setCode(200); + return responseDTO; + } + + /** + * 文件下载-311及之后版本 + * + * @param fileUrl 符合文件连接器规范的文件url + * @return base64String + */ + public InputStream fileDownloadV2(String fileUrl) { + try { + String path = dealPath(fileUrl); + // 获取文件连接器 + AbstractFileConnector defaultConnector = FileConnectionManager.getDefaultFileConnector(); + //将fileUrl构造成CodeWaveFileUrl + CodeWaveFileUrl fileUrlCodeWaveFileUrl = CodeWaveFileUrl.fromUri(path); + //调用制品配置的文件连接器的下载方法 + FileDownloadResult result = defaultConnector.download(fileUrlCodeWaveFileUrl, new HashMap<>()); + return result.getInputStream(); + } catch (Exception e) { + log.error("uploadBase64File error", e); + } + return null; + } + + public String dealPath(String path) { + path = path.replaceAll("/{2,}", "/"); + path = path.startsWith("/download") ? path.substring("/download".length()) : + path.substring(path.indexOf(UPLOAD_API_PATH_PREFIX) + UPLOAD_API_PATH_PREFIX.length()); + if (path.startsWith("/")) { + path = path.substring(1); + } + return path; + } +} diff --git a/itextpdf-tool/src/main/java/com/netease/lowcode/lib/file/UploadResponseDTO.java b/itextpdf-tool/src/main/java/com/netease/lowcode/lib/file/UploadResponseDTO.java new file mode 100644 index 000000000..7f28486a8 --- /dev/null +++ b/itextpdf-tool/src/main/java/com/netease/lowcode/lib/file/UploadResponseDTO.java @@ -0,0 +1,84 @@ +package com.netease.lowcode.lib.file; + +import com.netease.lowcode.core.annotation.NaslStructure; + +import java.io.Serializable; + +@NaslStructure +public class UploadResponseDTO implements Serializable { + public Integer code; + public String msg; + public String result; + public String filePath; + public Boolean success; + public String trace; + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public String getMsg() { + return msg; + } + + public void setMsg(String msg) { + this.msg = msg; + } + + 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; + } + + public Boolean getSuccess() { + return success; + } + + public void setSuccess(Boolean success) { + this.success = success; + } + + public String getTrace() { + return trace; + } + + public void setTrace(String trace) { + this.trace = trace; + } + + public static UploadResponseDTO FAIL(String msg) { + return FAIL(msg, null); + } + public static UploadResponseDTO FAIL(String msg, String trace) { + UploadResponseDTO fail = new UploadResponseDTO(); + fail.setSuccess(false); + fail.setMsg(msg); + fail.setTrace(trace); + fail.setCode(500); + return fail; + } + public static UploadResponseDTO OK(String path, String url) { + UploadResponseDTO callSuccess=new UploadResponseDTO(); + callSuccess.setFilePath(path); + callSuccess.setSuccess(true); + callSuccess.setResult(url); + callSuccess.setCode(200); + return callSuccess; + } + +} diff --git a/itextpdf-tool/src/main/java/com/netease/lowcode/lib/itextpdf/BaseFontBuilder.java b/itextpdf-tool/src/main/java/com/netease/lowcode/lib/itextpdf/BaseFontBuilder.java new file mode 100644 index 000000000..7ac350db5 --- /dev/null +++ b/itextpdf-tool/src/main/java/com/netease/lowcode/lib/itextpdf/BaseFontBuilder.java @@ -0,0 +1,35 @@ +package com.netease.lowcode.lib.itextpdf; + +import com.itextpdf.text.DocumentException; +import com.itextpdf.text.Font; +import com.itextpdf.text.pdf.BaseFont; +import com.netease.lowcode.lib.enums.BaseColorEnum; +import com.netease.lowcode.lib.enums.FontStyleEnum; +import com.netease.lowcode.lib.structure.ITextFontStructure; +import org.springframework.util.StringUtils; + +import java.io.IOException; + +public class BaseFontBuilder { + public static Font setFont(ITextFontStructure iTextFontStructure) throws DocumentException, IOException { + if (StringUtils.isEmpty(iTextFontStructure.getFontEncoding())) { + iTextFontStructure.setFontEncoding("UniGB-UCS2-H"); + } + if (iTextFontStructure.getEmbedded() == null) { + iTextFontStructure.setEmbedded(false); + } + if (iTextFontStructure.getForceRead() == null) { + iTextFontStructure.setForceRead(false); + } + if (iTextFontStructure.getSize() == null) { + iTextFontStructure.setSize(-1.0d); + } + if (StringUtils.isEmpty(iTextFontStructure.getFontName())) { + iTextFontStructure.setFontName("STSong-Light"); + } + BaseFont baseFont = BaseFont.createFont(iTextFontStructure.getFontName(), iTextFontStructure.getFontEncoding(), iTextFontStructure.getEmbedded(), iTextFontStructure.getForceRead()); + Integer style = FontStyleEnum.getFontStyle(iTextFontStructure.getStyle()); + return new Font(baseFont, Float.parseFloat(iTextFontStructure.getSize() + ""), style, BaseColorEnum.getBaseColor(iTextFontStructure.getColor())); + } + +} diff --git a/itextpdf-tool/src/main/java/com/netease/lowcode/lib/itextpdf/PdfBuilderUtil.java b/itextpdf-tool/src/main/java/com/netease/lowcode/lib/itextpdf/PdfBuilderUtil.java new file mode 100644 index 000000000..d857060c9 --- /dev/null +++ b/itextpdf-tool/src/main/java/com/netease/lowcode/lib/itextpdf/PdfBuilderUtil.java @@ -0,0 +1,275 @@ +package com.netease.lowcode.lib.itextpdf; + +import com.itextpdf.text.*; +import com.itextpdf.text.pdf.PdfPCell; +import com.itextpdf.text.pdf.PdfPTable; +import com.netease.lowcode.lib.enums.BaseColorEnum; +import com.netease.lowcode.lib.enums.ElementAlignEnum; +import com.netease.lowcode.lib.enums.RectangleEnum; +import com.netease.lowcode.lib.structure.*; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.List; + +public class PdfBuilderUtil { + + public static void fillParagraph(Paragraph paragraph, ITextParagraphStructure iTextParagraphStructure) { + if (iTextParagraphStructure == null) { + return; + } + if (iTextParagraphStructure.getAlignText() != null) { + paragraph.setAlignment(ElementAlignEnum.getAlignElement(iTextParagraphStructure.getAlignText())); + } + if (iTextParagraphStructure.getIndentationLeft() != null) { + paragraph.setIndentationLeft(Float.parseFloat(iTextParagraphStructure.getIndentationLeft() + "")); + } + if (iTextParagraphStructure.getIndentationRight() != null) { + paragraph.setIndentationRight(Float.parseFloat(iTextParagraphStructure.getIndentationRight() + "")); + } + if (iTextParagraphStructure.getFirstLineIndent() != null) { + paragraph.setFirstLineIndent(Float.parseFloat(iTextParagraphStructure.getFirstLineIndent() + "")); + } + if (iTextParagraphStructure.getSpacingBefore() != null) { + paragraph.setSpacingBefore(Float.parseFloat(iTextParagraphStructure.getSpacingBefore() + "")); + } + if (iTextParagraphStructure.getSpacingAfter() != null) { + paragraph.setSpacingAfter(Float.parseFloat(iTextParagraphStructure.getSpacingAfter() + "")); + } + if (iTextParagraphStructure.getFixedLeading() != null) { + paragraph.setLeading(Float.parseFloat(iTextParagraphStructure.getFixedLeading() + "")); + } + if (iTextParagraphStructure.getMultipliedLeading() != null) { + paragraph.setMultipliedLeading(Float.parseFloat(iTextParagraphStructure.getMultipliedLeading() + "")); + } + if (iTextParagraphStructure.getKeepTogether() != null) { + paragraph.setKeepTogether(iTextParagraphStructure.getKeepTogether()); + } + } + + public static void fillTable(PdfPTable table, ITextTableStructure iTextTableStructure) throws DocumentException { + if (iTextTableStructure == null) { + return; + } + if (iTextTableStructure.getWidthPercentage() != null) { + table.setWidthPercentage(Float.parseFloat(iTextTableStructure.getWidthPercentage() + "")); + } + if (iTextTableStructure.getTotalWidth() != null) { + table.setTotalWidth(Float.parseFloat(iTextTableStructure.getTotalWidth() + "")); + } + if (iTextTableStructure.getLockedWidth() != null) { + table.setLockedWidth(iTextTableStructure.getLockedWidth()); + } + if (iTextTableStructure.getHorizontalAlignmentText() != null) { + table.setHorizontalAlignment(ElementAlignEnum.getAlignElement(iTextTableStructure.getHorizontalAlignmentText())); + } + if (iTextTableStructure.getSpacingBefore() != null) { + table.setSpacingBefore(Float.parseFloat(iTextTableStructure.getSpacingBefore() + "")); + } + if (iTextTableStructure.getSpacingAfter() != null) { + table.setSpacingAfter(Float.parseFloat(iTextTableStructure.getSpacingAfter() + "")); + } + if (iTextTableStructure.getWidths() != null && !iTextTableStructure.getWidths().isEmpty()) { + float[] widths = new float[iTextTableStructure.getWidths().size()]; + for (int i = 0; i < iTextTableStructure.getWidths().size(); i++) { + widths[i] = Float.parseFloat(iTextTableStructure.getWidths().get(i) + ""); + } + table.setWidths(widths); + } + if (iTextTableStructure.getHeaderRows() != null) { + table.setHeaderRows(iTextTableStructure.getHeaderRows()); + } + if (iTextTableStructure.getFooterRows() != null) { + table.setFooterRows(iTextTableStructure.getFooterRows()); + } + if (iTextTableStructure.getSplitLate() != null) { + table.setSplitLate(iTextTableStructure.getSplitLate()); + } + if (iTextTableStructure.getSkipFirstHeader() != null) { + table.setSkipFirstHeader(iTextTableStructure.getSkipFirstHeader()); + } + } + + public static void fillCell(PdfPCell cell, ITextCellStructure iTextCellStructure) { + if (iTextCellStructure == null) { + return; + } + if (iTextCellStructure.getBorder() != null) { + cell.setBorder(RectangleEnum.getRectangle(iTextCellStructure.getBorder())); + } + if (iTextCellStructure.getBorderWidth() != null) { + cell.setBorderWidth(Float.parseFloat(iTextCellStructure.getBorderWidth() + "")); + } + if (iTextCellStructure.getPadding() != null) { + cell.setPadding(Float.parseFloat(iTextCellStructure.getPadding() + "")); + } + if (iTextCellStructure.getBorderColor() != null) { + cell.setBorderColor(BaseColorEnum.getBaseColor(iTextCellStructure.getBorderColor())); + } + if (iTextCellStructure.getPaddingLeft() != null) { + cell.setPaddingLeft(Float.parseFloat(iTextCellStructure.getPaddingLeft() + "")); + } + if (iTextCellStructure.getPaddingRight() != null) { + cell.setPaddingRight(Float.parseFloat(iTextCellStructure.getPaddingRight() + "")); + } + if (iTextCellStructure.getPaddingTop() != null) { + cell.setPaddingTop(Float.parseFloat(iTextCellStructure.getPaddingTop() + "")); + } + if (iTextCellStructure.getPaddingBottom() != null) { + cell.setPaddingBottom(Float.parseFloat(iTextCellStructure.getPaddingBottom() + "")); + } + if (iTextCellStructure.getHorizontalAlignmentText() != null) { + cell.setHorizontalAlignment(ElementAlignEnum.getAlignElement(iTextCellStructure.getHorizontalAlignmentText())); + } + if (iTextCellStructure.getVerticalAlignmentText() != null) { + cell.setVerticalAlignment(ElementAlignEnum.getAlignElement(iTextCellStructure.getVerticalAlignmentText())); + } + if (iTextCellStructure.getFixedHeight() != null) { + cell.setFixedHeight(Float.parseFloat(iTextCellStructure.getFixedHeight() + "")); + } + if (iTextCellStructure.getMinimumHeight() != null) { + cell.setMinimumHeight(Float.parseFloat(iTextCellStructure.getMinimumHeight() + "")); + } + if (iTextCellStructure.getFixedLeading() == null) { + iTextCellStructure.setFixedLeading(0d); + } + if (iTextCellStructure.getMultipliedLeading() == null) { + iTextCellStructure.setMultipliedLeading(0d); + } + cell.setLeading(Float.parseFloat(iTextCellStructure.getFixedLeading() + ""), Float.parseFloat(iTextCellStructure.getMultipliedLeading() + "")); + + if (iTextCellStructure.getBackgroundColor() != null) { + cell.setBackgroundColor(BaseColorEnum.getBaseColor(iTextCellStructure.getBackgroundColor())); + } + if (iTextCellStructure.getColSpan() != null) { + cell.setColspan(iTextCellStructure.getColSpan()); + } + if (iTextCellStructure.getRowSpan() != null) { + cell.setRowspan(iTextCellStructure.getRowSpan()); + } + } + + + public static void fillChunk(Chunk chunk, ITextChunkStructure iTextChunkStructure) { + if (iTextChunkStructure == null) { + return; + } + if (iTextChunkStructure.getCharacterSpacing() != null) { + chunk.setCharacterSpacing(Float.parseFloat(iTextChunkStructure.getCharacterSpacing() + "")); + } + if (iTextChunkStructure.getWordSpacing() != null) { + chunk.setWordSpacing(Float.parseFloat(iTextChunkStructure.getWordSpacing() + "")); + } + if (iTextChunkStructure.getAnchor() != null) { + chunk.setAnchor(iTextChunkStructure.getAnchor()); + } + if (iTextChunkStructure.getBackgroundColor() != null) { + chunk.setBackground(BaseColorEnum.getBaseColor(iTextChunkStructure.getBackgroundColor())); + } + } + + public static void fillPhrase(Phrase phrase, ITextPhraseStructure iTextPhraseStructure) { + if (iTextPhraseStructure == null) { + return; + } + if (iTextPhraseStructure.getFixedLeading() != null) { + phrase.setLeading(Float.parseFloat(iTextPhraseStructure.getFixedLeading() + "")); + } + if (iTextPhraseStructure.getMultipliedLeading() != null) { + phrase.setMultipliedLeading(Float.parseFloat(iTextPhraseStructure.getMultipliedLeading() + "")); + } + } + + public static void filListItem(ListItem listItem, ITextListStructure iTextListStructure) { + if (iTextListStructure == null) { + return; + } + if (iTextListStructure.getAlignText() != null) { + listItem.setAlignment(ElementAlignEnum.getAlignElement(iTextListStructure.getAlignText())); + } + if (iTextListStructure.getIndentationLeft() != null) { + listItem.setIndentationLeft(Float.parseFloat(iTextListStructure.getIndentationLeft() + "")); + } + if (iTextListStructure.getIndentationRight() != null) { + listItem.setIndentationRight(Float.parseFloat(iTextListStructure.getIndentationRight() + "")); + } + if (iTextListStructure.getFirstLineIndent() != null) { + listItem.setFirstLineIndent(Float.parseFloat(iTextListStructure.getFirstLineIndent() + "")); + } + if (iTextListStructure.getKeepTogether() != null) { + listItem.setKeepTogether(iTextListStructure.getKeepTogether()); + } + if (iTextListStructure.getSpacingBefore() != null) { + listItem.setSpacingBefore(Float.parseFloat(iTextListStructure.getSpacingBefore() + "")); + } + if (iTextListStructure.getSpacingAfter() != null) { + listItem.setSpacingAfter(Float.parseFloat(iTextListStructure.getSpacingAfter() + "")); + } + if (iTextListStructure.getPaddingTop() != null) { + listItem.setPaddingTop(Float.parseFloat(iTextListStructure.getPaddingTop() + "")); + } + if (iTextListStructure.getFixedLeading() != null) { + listItem.setLeading(Float.parseFloat(iTextListStructure.getFixedLeading() + "")); + } + if (iTextListStructure.getMultipliedLeading() != null) { + listItem.setMultipliedLeading(Float.parseFloat(iTextListStructure.getMultipliedLeading() + "")); + } + } + + public static void fillImage(Image image, ITextImageStructure iTextImageStructure) throws MalformedURLException { + if (iTextImageStructure == null) { + return; + } + if (iTextImageStructure.getScaleToFitHeight() != null && iTextImageStructure.getScaleToFitWidth() != null) { + image.scaleToFit(Float.parseFloat(iTextImageStructure.getScaleToFitWidth() + ""), Float.parseFloat(iTextImageStructure.getScaleToFitHeight() + "")); + } + + if (iTextImageStructure.getScaleAbsoluteWidth() != null && iTextImageStructure.getScaleAbsoluteHeight() == null) { + image.scaleAbsolute(Float.parseFloat(iTextImageStructure.getScaleAbsoluteWidth() + ""), Float.parseFloat(iTextImageStructure.getScaleAbsoluteHeight() + "")); + } + if (iTextImageStructure.getScalePercent() != null) { + image.scalePercent(Float.parseFloat(iTextImageStructure.getScalePercent() + "")); + } + if (iTextImageStructure.getAlignText() != null) { + image.setAlignment(ElementAlignEnum.getAlignElement(iTextImageStructure.getAlignText())); + } + if (iTextImageStructure.getAbsoluteX() != null && iTextImageStructure.getAbsoluteY() != null) { + image.setAbsolutePosition(Float.parseFloat(iTextImageStructure.getAbsoluteX() + ""), Float.parseFloat(iTextImageStructure.getAbsoluteY() + "")); + } + if (iTextImageStructure.getIndentationLeft() != null) { + image.setIndentationLeft(Float.parseFloat(iTextImageStructure.getIndentationLeft() + "")); + } + if (iTextImageStructure.getBorder() != null) { + image.setBorder(RectangleEnum.getRectangle(iTextImageStructure.getBorder())); + } + if (iTextImageStructure.getBorderWidth() != null) { + image.setBorderWidth(Float.parseFloat(iTextImageStructure.getBorderWidth() + "")); + } + if (iTextImageStructure.getBorderColor() != null) { + image.setBorderColor(BaseColorEnum.getBaseColor(iTextImageStructure.getBorderColor())); + } + if (iTextImageStructure.getSpacingBefore() != null) { + image.setSpacingBefore(Float.parseFloat(iTextImageStructure.getSpacingBefore() + "")); + } + if (iTextImageStructure.getSpacingAfter() != null) { + image.setSpacingAfter(Float.parseFloat(iTextImageStructure.getSpacingAfter() + "")); + } + if (iTextImageStructure.getTransparency() != null && !iTextImageStructure.getTransparency().isEmpty()) { + List transparencyList = iTextImageStructure.getTransparency(); + int[] transparency = new int[transparencyList.size()]; + for (int i = 0; i < transparencyList.size(); i++) { + transparency[i] = transparencyList.get(i); + } + image.setTransparency(transparency); + } + if (iTextImageStructure.getAnchor() != null) { + image.setUrl(new URL(iTextImageStructure.getAnchor())); + } + if (iTextImageStructure.getCompressionLevel() != null) { + image.setCompressionLevel(iTextImageStructure.getCompressionLevel()); + } + if (iTextImageStructure.getDpiX() != null && iTextImageStructure.getDpiY() != null) { + image.setDpi(iTextImageStructure.getDpiX(), iTextImageStructure.getDpiY()); + } + } +} diff --git a/itextpdf-tool/src/main/java/com/netease/lowcode/lib/itextpdf/PdfDocumentBuilder.java b/itextpdf-tool/src/main/java/com/netease/lowcode/lib/itextpdf/PdfDocumentBuilder.java new file mode 100644 index 000000000..ccaa8ec97 --- /dev/null +++ b/itextpdf-tool/src/main/java/com/netease/lowcode/lib/itextpdf/PdfDocumentBuilder.java @@ -0,0 +1,52 @@ +package com.netease.lowcode.lib.itextpdf; + +import com.itextpdf.text.Document; +import com.itextpdf.text.pdf.PdfWriter; + +public class PdfDocumentBuilder { + + + private Document document; + private String tempDirectoryFinal; + private String fileName; + private PdfWriter pdfWriter; + + public PdfDocumentBuilder(Document document, String tempDirectoryFinal, String fileName, PdfWriter pdfWriter) { + this.document = document; + this.tempDirectoryFinal = tempDirectoryFinal; + this.fileName = fileName; + this.pdfWriter = pdfWriter; + } + + public PdfWriter getPdfWriter() { + return pdfWriter; + } + + public void setPdfWriter(PdfWriter pdfWriter) { + this.pdfWriter = pdfWriter; + } + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public String getTempDirectoryFinal() { + return tempDirectoryFinal; + } + + public void setTempDirectoryFinal(String tempDirectoryFinal) { + this.tempDirectoryFinal = tempDirectoryFinal; + } + + public Document getDocument() { + return document; + } + + public void setDocument(Document document) { + this.document = document; + } +} diff --git a/itextpdf-tool/src/main/java/com/netease/lowcode/lib/structure/ITextCellStructure.java b/itextpdf-tool/src/main/java/com/netease/lowcode/lib/structure/ITextCellStructure.java new file mode 100644 index 000000000..a6031fa25 --- /dev/null +++ b/itextpdf-tool/src/main/java/com/netease/lowcode/lib/structure/ITextCellStructure.java @@ -0,0 +1,212 @@ +package com.netease.lowcode.lib.structure; + +import com.netease.lowcode.core.annotation.NaslStructure; + +@NaslStructure +public class ITextCellStructure { + /** + * 全局边框(UNDEFINED/TOP/BOTTOM/LEFT/RIGHT/NO_BORDER/BOX) + */ + public String border; + /** + * 单元格边框宽度 + */ + public Double borderWidth; + /** + * 单元格内边距 + */ + public Double padding; + /** + * 边框颜色,枚举:WHITE/LIGHT_GRAY/GRAY/DARK_GRAY/BLACK/RED/PINK/ORANGE/YELLOW/GREEN/MAGENTA/CYAN/BLUE + */ + public String borderColor; + /** + * 左边距 + */ + public Double paddingLeft; + /** + * 右边距 + */ + public Double paddingRight; + /** + * 上边距 + */ + public Double paddingTop; + /** + * 下边距 + */ + public Double paddingBottom; + /** + * 水平对齐方式(ALIGN_UNDEFINED/ALIGN_LEFT/ALIGN_CENTER/ALIGN_RIGHT/ALIGN_JUSTIFIED/ALIGN_TOP/ALIGN_MIDDLE/ALIGN_BOTTOM/ALIGN_BASELINE/ALIGN_JUSTIFIED_ALL) + */ + public String horizontalAlignmentText; + /** + * 垂直对齐方式(ALIGN_UNDEFINED/ALIGN_LEFT/ALIGN_CENTER/ALIGN_RIGHT/ALIGN_JUSTIFIED/ALIGN_TOP/ALIGN_MIDDLE/ALIGN_BOTTOM/ALIGN_BASELINE/ALIGN_JUSTIFIED_ALL) + */ + public String verticalAlignmentText; + /** + * 固定高度 + */ + public Double fixedHeight; + /** + * 最小高度 + */ + public Double minimumHeight; + /** + * 行间距(固定值) + */ + public Double fixedLeading; + /** + * 行间距(倍数) + */ + public Double multipliedLeading; + /** + * 背景颜色,枚举:WHITE/LIGHT_GRAY/GRAY/DARK_GRAY/BLACK/RED/PINK/ORANGE/YELLOW/GREEN/MAGENTA/CYAN/BLUE + */ + public String backgroundColor; + /** + * 横向合并列数 + */ + public Integer colSpan; + /** + * 纵向合并行数 + */ + public Integer rowSpan; + + public String getVerticalAlignmentText() { + return verticalAlignmentText; + } + + public void setVerticalAlignmentText(String verticalAlignmentText) { + this.verticalAlignmentText = verticalAlignmentText; + } + + public String getHorizontalAlignmentText() { + return horizontalAlignmentText; + } + + public void setHorizontalAlignmentText(String horizontalAlignmentText) { + this.horizontalAlignmentText = horizontalAlignmentText; + } + + + public Double getFixedHeight() { + return fixedHeight; + } + + public void setFixedHeight(Double fixedHeight) { + this.fixedHeight = fixedHeight; + } + + public Double getMinimumHeight() { + return minimumHeight; + } + + public void setMinimumHeight(Double minimumHeight) { + this.minimumHeight = minimumHeight; + } + + public Double getFixedLeading() { + return fixedLeading; + } + + public void setFixedLeading(Double fixedLeading) { + this.fixedLeading = fixedLeading; + } + + public Double getMultipliedLeading() { + return multipliedLeading; + } + + public void setMultipliedLeading(Double multipliedLeading) { + this.multipliedLeading = multipliedLeading; + } + + public String getBackgroundColor() { + return backgroundColor; + } + + public void setBackgroundColor(String backgroundColor) { + this.backgroundColor = backgroundColor; + } + + public Integer getColSpan() { + return colSpan; + } + + public void setColSpan(Integer colSpan) { + this.colSpan = colSpan; + } + + public Integer getRowSpan() { + return rowSpan; + } + + public void setRowSpan(Integer rowSpan) { + this.rowSpan = rowSpan; + } + + public String getBorder() { + return border; + } + + public void setBorder(String border) { + this.border = border; + } + + public Double getBorderWidth() { + return borderWidth; + } + + public void setBorderWidth(Double borderWidth) { + this.borderWidth = borderWidth; + } + + public Double getPadding() { + return padding; + } + + public void setPadding(Double padding) { + this.padding = padding; + } + + public String getBorderColor() { + return borderColor; + } + + public void setBorderColor(String borderColor) { + this.borderColor = borderColor; + } + + public Double getPaddingLeft() { + return paddingLeft; + } + + public void setPaddingLeft(Double paddingLeft) { + this.paddingLeft = paddingLeft; + } + + public Double getPaddingRight() { + return paddingRight; + } + + public void setPaddingRight(Double paddingRight) { + this.paddingRight = paddingRight; + } + + public Double getPaddingTop() { + return paddingTop; + } + + public void setPaddingTop(Double paddingTop) { + this.paddingTop = paddingTop; + } + + public Double getPaddingBottom() { + return paddingBottom; + } + + public void setPaddingBottom(Double paddingBottom) { + this.paddingBottom = paddingBottom; + } +} diff --git a/itextpdf-tool/src/main/java/com/netease/lowcode/lib/structure/ITextChunkStructure.java b/itextpdf-tool/src/main/java/com/netease/lowcode/lib/structure/ITextChunkStructure.java new file mode 100644 index 000000000..7a8e4e860 --- /dev/null +++ b/itextpdf-tool/src/main/java/com/netease/lowcode/lib/structure/ITextChunkStructure.java @@ -0,0 +1,55 @@ +package com.netease.lowcode.lib.structure; + +import com.netease.lowcode.core.annotation.NaslStructure; + +@NaslStructure +public class ITextChunkStructure { + /** + * 设置字符间距 + */ + public Double characterSpacing; + /** + * 添加超链接 + */ + public String anchor; + /** + * 设置单词间距(仅对空格有效) + */ + public Double wordSpacing; + /** + * 设置背景色 + */ + public String backgroundColor; + + public Double getCharacterSpacing() { + return characterSpacing; + } + + public void setCharacterSpacing(Double characterSpacing) { + this.characterSpacing = characterSpacing; + } + + public String getAnchor() { + return anchor; + } + + public void setAnchor(String anchor) { + this.anchor = anchor; + } + + public Double getWordSpacing() { + return wordSpacing; + } + + public void setWordSpacing(Double wordSpacing) { + this.wordSpacing = wordSpacing; + } + + public String getBackgroundColor() { + return backgroundColor; + } + + public void setBackgroundColor(String backgroundColor) { + this.backgroundColor = backgroundColor; + } +} diff --git a/itextpdf-tool/src/main/java/com/netease/lowcode/lib/structure/ITextFontStructure.java b/itextpdf-tool/src/main/java/com/netease/lowcode/lib/structure/ITextFontStructure.java new file mode 100644 index 000000000..f17df02dd --- /dev/null +++ b/itextpdf-tool/src/main/java/com/netease/lowcode/lib/structure/ITextFontStructure.java @@ -0,0 +1,98 @@ +package com.netease.lowcode.lib.structure; + +import com.itextpdf.text.DocumentException; +import com.itextpdf.text.pdf.BaseFont; +import com.netease.lowcode.core.annotation.NaslStructure; +import org.springframework.util.StringUtils; + +import java.io.IOException; + +@NaslStructure +public class ITextFontStructure { + /** + * 字体名称,默认STSong-Light + */ + public String fontName; + /** + * 字体编码,默认UniGB-UCS2-H + */ + public String fontEncoding; + /** + * 是否嵌入字体:true表示将字体文件嵌入文档(确保跨设备显示一致),false表示不嵌入 + */ + public Boolean embedded; + /** + * 强制读取字体:true表示忽略系统缓存强制重新加载字体文件(解决字体更新问题) + */ + public Boolean forceRead; + /** + * 字体大小(单位:磅/pt),例如 12.0 表示12磅字号 + */ + public Double size; + /** + * 样式,枚举:NORMAL/BOLD/ITALIC/UNDERLINE/STRIKETHRU/BOLDITALIC/UNDEFINED/DEFAULTSIZE + */ + public String style; + /** + * 颜色,枚举:WHITE/LIGHT_GRAY/GRAY/DARK_GRAY/BLACK/RED/PINK/ORANGE/YELLOW/GREEN/MAGENTA/CYAN/BLUE + */ + public String color; + + public String getFontName() { + return fontName; + } + + public void setFontName(String fontName) { + this.fontName = fontName; + } + + public String getFontEncoding() { + return fontEncoding; + } + + public void setFontEncoding(String fontEncoding) { + this.fontEncoding = fontEncoding; + } + + public Boolean getEmbedded() { + return embedded; + } + + public void setEmbedded(Boolean embedded) { + this.embedded = embedded; + } + + public Boolean getForceRead() { + return forceRead; + } + + public void setForceRead(Boolean forceRead) { + this.forceRead = forceRead; + } + + + public Double getSize() { + return size; + } + + public void setSize(Double size) { + this.size = size; + } + + public String getColor() { + return color; + } + + public void setColor(String color) { + this.color = color; + } + + public String getStyle() { + return style; + } + + public void setStyle(String style) { + this.style = style; + } + +} diff --git a/itextpdf-tool/src/main/java/com/netease/lowcode/lib/structure/ITextImageStructure.java b/itextpdf-tool/src/main/java/com/netease/lowcode/lib/structure/ITextImageStructure.java new file mode 100644 index 000000000..86588d318 --- /dev/null +++ b/itextpdf-tool/src/main/java/com/netease/lowcode/lib/structure/ITextImageStructure.java @@ -0,0 +1,239 @@ +package com.netease.lowcode.lib.structure; + +import com.netease.lowcode.core.annotation.NaslStructure; + +import java.util.List; + +@NaslStructure +public class ITextImageStructure { + /** + * 按比例缩放图片到目标尺寸(保持宽高比) + */ + public Double scaleToFitHeight; + /** + * 按比例缩放图片到目标尺寸(保持宽高比) + */ + public Double scaleToFitWidth; + /** + * 缩放图片的百分比 + */ + public Double scalePercent; + /** + * 固定宽度,高度按比例调整。 + */ + public Double scaleAbsoluteWidth; + /** + * 固定高度,宽度按比例调整。 + */ + public Double scaleAbsoluteHeight; + /** + * 对齐方式 + * ALIGN_UNDEFINED/ALIGN_LEFT/ALIGN_CENTER/ALIGN_RIGHT/ALIGN_JUSTIFIED/ALIGN_TOP/ALIGN_MIDDLE/ALIGN_BOTTOM/ALIGN_BASELINE/ALIGN_JUSTIFIED_ALL + */ + public String alignText; + /** + * 设置绝对坐标(相对于页面左下角) + */ + public Double absoluteX; + /** + * 设置绝对坐标(相对于页面左下角) + */ + public Double absoluteY; + /** + * 左缩进(需在非绝对定位模式下使用) + */ + public Double indentationLeft; + /** + * 全局边框(UNDEFINED/TOP/BOTTOM/LEFT/RIGHT/NO_BORDER/BOX) + */ + public String border; + /** + * 边框宽度 + */ + public Double borderWidth; + + /** + * 边框颜色,枚举:WHITE/LIGHT_GRAY/GRAY/DARK_GRAY/BLACK/RED/PINK/ORANGE/YELLOW/GREEN/MAGENTA/CYAN/BLUE + */ + public String borderColor; + /** + * 图片上方的空白间距 + */ + public Double spacingBefore; + /** + * 图片下方的空白间距 + */ + public Double spacingAfter; + /** + * 置透明度(ARGB值),示例:{200, 255, 255, 255} + */ + public List transparency; + /** + * 设置超链接(点击跳转URL) + */ + public String anchor; + /** + * 设置JPEG压缩质量(0-9,0=最低压缩) + */ + public Integer compressionLevel; + /** + * 设置DPI(影响打印质量) + */ + public Integer dpiX; + /** + * 设置DPI(影响打印质量) + */ + public Integer dpiY; + + public Double getScaleToFitHeight() { + return scaleToFitHeight; + } + + public void setScaleToFitHeight(Double scaleToFitHeight) { + this.scaleToFitHeight = scaleToFitHeight; + } + + public Double getScaleToFitWidth() { + return scaleToFitWidth; + } + + public void setScaleToFitWidth(Double scaleToFitWidth) { + this.scaleToFitWidth = scaleToFitWidth; + } + + public Double getScalePercent() { + return scalePercent; + } + + public void setScalePercent(Double scalePercent) { + this.scalePercent = scalePercent; + } + + public Double getScaleAbsoluteWidth() { + return scaleAbsoluteWidth; + } + + public void setScaleAbsoluteWidth(Double scaleAbsoluteWidth) { + this.scaleAbsoluteWidth = scaleAbsoluteWidth; + } + + public Double getScaleAbsoluteHeight() { + return scaleAbsoluteHeight; + } + + public void setScaleAbsoluteHeight(Double scaleAbsoluteHeight) { + this.scaleAbsoluteHeight = scaleAbsoluteHeight; + } + + public String getAlignText() { + return alignText; + } + + public void setAlignText(String alignText) { + this.alignText = alignText; + } + + public Double getAbsoluteX() { + return absoluteX; + } + + public void setAbsoluteX(Double absoluteX) { + this.absoluteX = absoluteX; + } + + public Double getAbsoluteY() { + return absoluteY; + } + + public void setAbsoluteY(Double absoluteY) { + this.absoluteY = absoluteY; + } + + public Double getIndentationLeft() { + return indentationLeft; + } + + public void setIndentationLeft(Double indentationLeft) { + this.indentationLeft = indentationLeft; + } + + public String getBorder() { + return border; + } + + public void setBorder(String border) { + this.border = border; + } + + public Double getBorderWidth() { + return borderWidth; + } + + public void setBorderWidth(Double borderWidth) { + this.borderWidth = borderWidth; + } + + public String getBorderColor() { + return borderColor; + } + + public void setBorderColor(String borderColor) { + this.borderColor = borderColor; + } + + public Double getSpacingBefore() { + return spacingBefore; + } + + public void setSpacingBefore(Double spacingBefore) { + this.spacingBefore = spacingBefore; + } + + public Double getSpacingAfter() { + return spacingAfter; + } + + public void setSpacingAfter(Double spacingAfter) { + this.spacingAfter = spacingAfter; + } + + public List getTransparency() { + return transparency; + } + + public void setTransparency(List transparency) { + this.transparency = transparency; + } + + public String getAnchor() { + return anchor; + } + + public void setAnchor(String anchor) { + this.anchor = anchor; + } + + public Integer getCompressionLevel() { + return compressionLevel; + } + + public void setCompressionLevel(Integer compressionLevel) { + this.compressionLevel = compressionLevel; + } + + public Integer getDpiX() { + return dpiX; + } + + public void setDpiX(Integer dpiX) { + this.dpiX = dpiX; + } + + public Integer getDpiY() { + return dpiY; + } + + public void setDpiY(Integer dpiY) { + this.dpiY = dpiY; + } +} diff --git a/itextpdf-tool/src/main/java/com/netease/lowcode/lib/structure/ITextListStructure.java b/itextpdf-tool/src/main/java/com/netease/lowcode/lib/structure/ITextListStructure.java new file mode 100644 index 000000000..242bf68b1 --- /dev/null +++ b/itextpdf-tool/src/main/java/com/netease/lowcode/lib/structure/ITextListStructure.java @@ -0,0 +1,136 @@ +package com.netease.lowcode.lib.structure; + +import com.netease.lowcode.core.annotation.NaslStructure; + +@NaslStructure +public class ITextListStructure { + /** + * 对齐方式 + * ALIGN_UNDEFINED/ALIGN_LEFT/ALIGN_CENTER/ALIGN_RIGHT/ALIGN_JUSTIFIED/ALIGN_TOP/ALIGN_MIDDLE/ALIGN_BOTTOM/ALIGN_BASELINE/ALIGN_JUSTIFIED_ALL + */ + public String alignText; + + /** + * 左缩进(单位:像素) + */ + public Double indentationLeft; + + /** + * 右缩进(单位:像素) + */ + public Double indentationRight; + + + /** + * 首行缩进 + */ + public Double firstLineIndent; + + /** + * 是否禁止分页时拆分段落(true 表示整段必须在同一页)] + */ + public Boolean keepTogether; + /** + * 段前间距 + */ + public Double spacingBefore; + + /** + * 段后间距 + */ + public Double spacingAfter; + + /** + * 上边距 + */ + public Double paddingTop; + + /** + * 行间距(固定值) + */ + public Double fixedLeading; + /** + * 行间距(倍数) + */ + public Double multipliedLeading; + + public String getAlignText() { + return alignText; + } + + public void setAlignText(String alignText) { + this.alignText = alignText; + } + + public Double getIndentationLeft() { + return indentationLeft; + } + + public void setIndentationLeft(Double indentationLeft) { + this.indentationLeft = indentationLeft; + } + + public Double getIndentationRight() { + return indentationRight; + } + + public void setIndentationRight(Double indentationRight) { + this.indentationRight = indentationRight; + } + + public Double getFirstLineIndent() { + return firstLineIndent; + } + + public void setFirstLineIndent(Double firstLineIndent) { + this.firstLineIndent = firstLineIndent; + } + + public Boolean getKeepTogether() { + return keepTogether; + } + + public void setKeepTogether(Boolean keepTogether) { + this.keepTogether = keepTogether; + } + + public Double getSpacingBefore() { + return spacingBefore; + } + + public void setSpacingBefore(Double spacingBefore) { + this.spacingBefore = spacingBefore; + } + + public Double getSpacingAfter() { + return spacingAfter; + } + + public void setSpacingAfter(Double spacingAfter) { + this.spacingAfter = spacingAfter; + } + + public Double getPaddingTop() { + return paddingTop; + } + + public void setPaddingTop(Double paddingTop) { + this.paddingTop = paddingTop; + } + + public Double getFixedLeading() { + return fixedLeading; + } + + public void setFixedLeading(Double fixedLeading) { + this.fixedLeading = fixedLeading; + } + + public Double getMultipliedLeading() { + return multipliedLeading; + } + + public void setMultipliedLeading(Double multipliedLeading) { + this.multipliedLeading = multipliedLeading; + } +} diff --git a/itextpdf-tool/src/main/java/com/netease/lowcode/lib/structure/ITextParagraphStructure.java b/itextpdf-tool/src/main/java/com/netease/lowcode/lib/structure/ITextParagraphStructure.java new file mode 100644 index 000000000..8731dd076 --- /dev/null +++ b/itextpdf-tool/src/main/java/com/netease/lowcode/lib/structure/ITextParagraphStructure.java @@ -0,0 +1,120 @@ +package com.netease.lowcode.lib.structure; + +import com.netease.lowcode.core.annotation.NaslStructure; + +@NaslStructure +public class ITextParagraphStructure { + /** + * 对齐方式 + * ALIGN_UNDEFINED/ALIGN_LEFT/ALIGN_CENTER/ALIGN_RIGHT/ALIGN_JUSTIFIED/ALIGN_TOP/ALIGN_MIDDLE/ALIGN_BOTTOM/ALIGN_BASELINE/ALIGN_JUSTIFIED_ALL + */ + public String alignText; + /** + * 左缩进(单位:像素) + */ + public Double indentationLeft; + + /** + * 右缩进(单位:像素) + */ + public Double indentationRight; + + /** + * 首行缩进 + */ + public Double firstLineIndent; + + /** + * 段前间距 + */ + public Double spacingBefore; + + /** + * 段后间距 + */ + public Double spacingAfter; + /** + * 是否禁止分页时拆分段落(true 表示整段必须在同一页)] + */ + public Boolean keepTogether; + /** + * 行间距(固定值) + */ + public Double fixedLeading; + /** + * 行间距(倍数) + */ + public Double multipliedLeading; + + public String getAlignText() { + return alignText; + } + + public void setAlignText(String alignText) { + this.alignText = alignText; + } + + public Double getIndentationLeft() { + return indentationLeft; + } + + public void setIndentationLeft(Double indentationLeft) { + this.indentationLeft = indentationLeft; + } + + public Double getIndentationRight() { + return indentationRight; + } + + public void setIndentationRight(Double indentationRight) { + this.indentationRight = indentationRight; + } + + public Double getFirstLineIndent() { + return firstLineIndent; + } + + public void setFirstLineIndent(Double firstLineIndent) { + this.firstLineIndent = firstLineIndent; + } + + public Double getSpacingBefore() { + return spacingBefore; + } + + public void setSpacingBefore(Double spacingBefore) { + this.spacingBefore = spacingBefore; + } + + public Double getSpacingAfter() { + return spacingAfter; + } + + public void setSpacingAfter(Double spacingAfter) { + this.spacingAfter = spacingAfter; + } + + public Double getFixedLeading() { + return fixedLeading; + } + + public void setFixedLeading(Double fixedLeading) { + this.fixedLeading = fixedLeading; + } + + public Double getMultipliedLeading() { + return multipliedLeading; + } + + public void setMultipliedLeading(Double multipliedLeading) { + this.multipliedLeading = multipliedLeading; + } + + public Boolean getKeepTogether() { + return keepTogether; + } + + public void setKeepTogether(Boolean keepTogether) { + this.keepTogether = keepTogether; + } +} diff --git a/itextpdf-tool/src/main/java/com/netease/lowcode/lib/structure/ITextPhraseStructure.java b/itextpdf-tool/src/main/java/com/netease/lowcode/lib/structure/ITextPhraseStructure.java new file mode 100644 index 000000000..111a86f26 --- /dev/null +++ b/itextpdf-tool/src/main/java/com/netease/lowcode/lib/structure/ITextPhraseStructure.java @@ -0,0 +1,32 @@ +package com.netease.lowcode.lib.structure; + +import com.netease.lowcode.core.annotation.NaslStructure; + +@NaslStructure +public class ITextPhraseStructure { + + /** + * 行间距(固定值) + */ + public Double fixedLeading; + /** + * 行间距(倍数) + */ + public Double multipliedLeading; + + public Double getFixedLeading() { + return fixedLeading; + } + + public void setFixedLeading(Double fixedLeading) { + this.fixedLeading = fixedLeading; + } + + public Double getMultipliedLeading() { + return multipliedLeading; + } + + public void setMultipliedLeading(Double multipliedLeading) { + this.multipliedLeading = multipliedLeading; + } +} diff --git a/itextpdf-tool/src/main/java/com/netease/lowcode/lib/structure/ITextTableStructure.java b/itextpdf-tool/src/main/java/com/netease/lowcode/lib/structure/ITextTableStructure.java new file mode 100644 index 000000000..bcfc3f3b6 --- /dev/null +++ b/itextpdf-tool/src/main/java/com/netease/lowcode/lib/structure/ITextTableStructure.java @@ -0,0 +1,154 @@ +package com.netease.lowcode.lib.structure; + +import com.netease.lowcode.core.annotation.NaslStructure; + +import java.util.List; + +@NaslStructure +public class ITextTableStructure { + /** + * 表格宽度占页面宽度的百分比(如 100%) + */ + public Double widthPercentage; + /** + * 表格绝对宽度(单位:像素) + */ + public Double totalWidth; + /** + * 是否固定表格宽度(true 时忽略自动调整) + */ + public Boolean lockedWidth; + /** + * 水平对齐方式(ALIGN_UNDEFINED/ALIGN_LEFT/ALIGN_CENTER/ALIGN_RIGHT/ALIGN_JUSTIFIED/ALIGN_TOP/ALIGN_MIDDLE/ALIGN_BOTTOM/ALIGN_BASELINE/ALIGN_JUSTIFIED_ALL) + */ + public String horizontalAlignmentText; + /** + * 表格前的空白间距 + */ + public Double spacingBefore; + /** + * 表格后的空白间距 + */ + public Double spacingAfter; + /** + * 表格列数 + */ + public Integer numColumns; + /** + * 每列宽度比例,数组大小与numColumns一致(如 new Double[]{1, 2, 3} 表示三列比例为1:2:3) + */ + public List widths; + /** + * 指定前N行为表头(跨页时自动重复) + */ + public Integer headerRows; + /** + * 指定后N行为表尾(跨页时自动重复) + */ + public Integer footerRows; + + /** + * 是否允许拆分单元格(false 时强制整单元格换页) + */ + public Boolean splitLate; + /** + * 是否跳过第一页的表头(用于连续表格) + */ + public Boolean skipFirstHeader; + + public Integer getNumColumns() { + return numColumns; + } + + public void setNumColumns(Integer numColumns) { + this.numColumns = numColumns; + } + + public Double getWidthPercentage() { + return widthPercentage; + } + + public void setWidthPercentage(Double widthPercentage) { + this.widthPercentage = widthPercentage; + } + + public Double getTotalWidth() { + return totalWidth; + } + + public void setTotalWidth(Double totalWidth) { + this.totalWidth = totalWidth; + } + + public Boolean getLockedWidth() { + return lockedWidth; + } + + public void setLockedWidth(Boolean lockedWidth) { + this.lockedWidth = lockedWidth; + } + + public String getHorizontalAlignmentText() { + return horizontalAlignmentText; + } + + public void setHorizontalAlignmentText(String horizontalAlignmentText) { + this.horizontalAlignmentText = horizontalAlignmentText; + } + + public Double getSpacingBefore() { + return spacingBefore; + } + + public void setSpacingBefore(Double spacingBefore) { + this.spacingBefore = spacingBefore; + } + + public Double getSpacingAfter() { + return spacingAfter; + } + + public void setSpacingAfter(Double spacingAfter) { + this.spacingAfter = spacingAfter; + } + + public List getWidths() { + return widths; + } + + public void setWidths(List widths) { + this.widths = widths; + } + + public Integer getHeaderRows() { + return headerRows; + } + + public void setHeaderRows(Integer headerRows) { + this.headerRows = headerRows; + } + + public Integer getFooterRows() { + return footerRows; + } + + public void setFooterRows(Integer footerRows) { + this.footerRows = footerRows; + } + + public Boolean getSplitLate() { + return splitLate; + } + + public void setSplitLate(Boolean splitLate) { + this.splitLate = splitLate; + } + + public Boolean getSkipFirstHeader() { + return skipFirstHeader; + } + + public void setSkipFirstHeader(Boolean skipFirstHeader) { + this.skipFirstHeader = skipFirstHeader; + } +} diff --git a/itextpdf-tool/src/main/resources/META-INF/spring.factories b/itextpdf-tool/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..fbebf46ce --- /dev/null +++ b/itextpdf-tool/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +com.netease.lowcode.lib.ItextpdfToolBasicSpringEnvironmentConfiguration \ No newline at end of file diff --git a/redis-template-tool/pom.xml b/redis-template-tool/pom.xml index 32ba1db1d..c3272a748 100644 --- a/redis-template-tool/pom.xml +++ b/redis-template-tool/pom.xml @@ -5,7 +5,7 @@ com.netease.lowcode redis-template-tool - 1.1.0 + 1.2.0 org.springframework.boot spring-boot-starter-parent @@ -63,12 +63,6 @@ org.apache.commons commons-pool2 - - com.alibaba.fastjson2 - fastjson2 - 2.0.47 - provided - org.apache.commons commons-lang3 @@ -90,7 +84,7 @@ 1.5.1 false - + true diff --git a/redis-template-tool/src/main/java/com/netease/lib/redistemplatetool/util/RedisTool.java b/redis-template-tool/src/main/java/com/netease/lib/redistemplatetool/util/RedisTool.java index f29969847..877edb351 100644 --- a/redis-template-tool/src/main/java/com/netease/lib/redistemplatetool/util/RedisTool.java +++ b/redis-template-tool/src/main/java/com/netease/lib/redistemplatetool/util/RedisTool.java @@ -1,6 +1,6 @@ package com.netease.lib.redistemplatetool.util; -import com.alibaba.fastjson2.JSONObject; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.netease.lowcode.core.annotation.NaslLogic; import org.slf4j.Logger; @@ -197,6 +197,32 @@ public Boolean setIfAbsentWithExpire(String key, String value, Long timeout) { return ops.setIfAbsent(key, value, timeout, TimeUnit.SECONDS); } + + /** + * 如果 Redis 中不存在指定 key,则设置为指定字符串,并设置过期时间 + * + * @param key Redis 中的键 + * @param value Redis 中的值 + * @param timeout 过期时间,单位s + * @param retryTimeout 重试持续时间,单位ms。默认无 + * @param retryIntervalTime 重试间隔时间,单位ms。默认10ms + * @return 如果设置成功,返回 true,否则返回 false + */ + @NaslLogic + public Boolean setIfAbsentWithExpireRetry(String key, String value, Long timeout, Long retryTimeout, Long retryIntervalTime) { + if (retryTimeout == null || retryTimeout <= 0) { + return setIfAbsentWithExpire(key, value, timeout); + } + + while (retryTimeout > 0) { + retryTimeout -= retryIntervalTime; + if (setIfAbsentWithExpire(key, value, timeout)) { + return true; + } + } + return false; + } + /** * 如果 Redis 中存在指定 key,则设置为指定字符串,并设置过期时间 * @@ -445,7 +471,7 @@ public List getHashTopN(String hashKey, Integer limit, String keywordVal } else { return allList.stream().filter(json -> { try { - JSONObject jsonObject = mapper.readValue(json, JSONObject.class); + JsonNode jsonObject = mapper.readValue(json, JsonNode.class); String nameValue = getKeywordValue(keywordName, jsonObject); return !StringUtils.isEmpty(nameValue) && nameValue.contains(keywordValue); } catch (Exception e) { @@ -457,9 +483,9 @@ public List getHashTopN(String hashKey, Integer limit, String keywordVal // 创建中文拼音排序器 Collator collator = Collator.getInstance(Locale.CHINA); // 获取两个json对象的中文名 - JSONObject jsonObject1 = mapper.readValue(json1, JSONObject.class); + JsonNode jsonObject1 = mapper.readValue(json1, JsonNode.class); String name1 = getKeywordValue(keywordName, jsonObject1); - JSONObject jsonObject2 = mapper.readValue(json2, JSONObject.class); + JsonNode jsonObject2 = mapper.readValue(json2, JsonNode.class); String name2 = getKeywordValue(keywordName, jsonObject2); // 使用Collator比较中文名 return collator.compare(name1, name2); @@ -472,24 +498,43 @@ public List getHashTopN(String hashKey, Integer limit, String keywordVal } } - private String getKeywordValue(String keywordName, JSONObject jsonObject) { + /** + * 使用Jackson JsonNode获取嵌套JSON字段值 + * + * @param keywordName 字段路径,如 "user.profile.name" + * @param jsonNode Jackson JsonNode对象 + * @return 字段值,如果不存在返回null + */ + private String getKeywordValue(String keywordName, JsonNode jsonNode) { + if (jsonNode == null || keywordName == null || keywordName.isEmpty()) { + return null; + } + String[] keywordNames = keywordName.split("\\."); - String nameValue; - if (keywordNames.length > 1) { - JSONObject keywordJsonObject = jsonObject; - for (int i = 0; i < keywordNames.length - 1; i++) { - String name = keywordNames[i]; - if (keywordJsonObject.containsKey(name)) { - keywordJsonObject = keywordJsonObject.getJSONObject(name); - } else { - break; - } + JsonNode currentNode = jsonNode; + + // 遍历路径的中间部分 + for (int i = 0; i < keywordNames.length - 1; i++) { + String name = keywordNames[i]; + if (currentNode.has(name) && currentNode.get(name).isObject()) { + currentNode = currentNode.get(name); + } else { + return null; // 路径中间部分不存在或不是对象 + } + } + + // 获取最后一级字段的值 + String lastName = keywordNames[keywordNames.length - 1]; + if (currentNode.has(lastName)) { + JsonNode valueNode = currentNode.get(lastName); + if (valueNode.isTextual()) { + return valueNode.asText(); + } else { + return valueNode.toString(); // 非文本类型转换为字符串 } - nameValue = keywordJsonObject.getString(keywordNames[keywordNames.length - 1]); - } else { - nameValue = jsonObject.getString(keywordName); } - return nameValue; + + return null; } /** @@ -521,7 +566,7 @@ public List getHashTopNByKeyMap(String hashKey, Integer limit, Map { try { - JSONObject jsonObject = mapper.readValue(json, JSONObject.class); + JsonNode jsonObject = mapper.readValue(json, JsonNode.class); return keywordMap.entrySet().stream().allMatch(entry -> { String jsonValue = getKeywordValue(entry.getKey(), jsonObject); return !StringUtils.isEmpty(jsonValue) && @@ -536,11 +581,11 @@ public List getHashTopNByKeyMap(String hashKey, Integer limit, Map redisTemplate; + + /** + * 设置候选列表(自动处理列表变化) + */ + @NaslLogic + public Boolean pollingSetCandidates(String nodeId, List candidates) { + String candidateKey = "node:" + nodeId + ":candidates"; + // 删除旧列表,设置新列表 + redisTemplate.delete(candidateKey); + if (!candidates.isEmpty()) { + redisTemplate.opsForList().rightPushAll(candidateKey, candidates); + } + return true; + } + + /** + * 选择下一个候选 + */ + @NaslLogic + public String pollingSelectNext(String nodeId) { + String candidateKey = "node:" + nodeId + ":candidates"; + String lastKey = "node:" + nodeId + ":last"; + + // 获取当前列表 + List candidates = redisTemplate.opsForList().range(candidateKey, 0, -1); + if (candidates == null || candidates.isEmpty()) { + throw new RuntimeException("没有可用的候选人"); + } + + // 获取上次分配的人 + String last = redisTemplate.opsForValue().get(lastKey); + + // 找到下一个候选人 + String next = findNextCandidate(candidates, last); + + // 记录这次分配的人 + redisTemplate.opsForValue().set(lastKey, next); + + return next; + } + + /** + * 简单的轮询查找逻辑 + */ + private String findNextCandidate(List candidates, String last) { + if (last == null || last.isEmpty()) { + return candidates.get(0); // 第一次从第一个开始 + } + + // 找上次分配的人在列表中的位置 + int lastIndex = -1; + for (int i = 0; i < candidates.size(); i++) { + if (candidates.get(i).equals(last)) { + lastIndex = i; + break; + } + } + + // 如果没找到或者是在最后,就从第一个开始 + if (lastIndex == -1 || lastIndex == candidates.size() - 1) { + return candidates.get(0); + } + + // 否则选下一个 + return candidates.get(lastIndex + 1); + } + + /** + * 获取当前候选人列表 + */ + public List getCandidates(String nodeId) { + String candidateKey = "node:" + nodeId + ":candidates"; + return redisTemplate.opsForList().range(candidateKey, 0, -1); + } +} \ No newline at end of file diff --git a/xxl-job/pom.xml b/xxl-job/pom.xml index d315d23de..2802ef834 100644 --- a/xxl-job/pom.xml +++ b/xxl-job/pom.xml @@ -12,7 +12,7 @@ com.netease.lowcode xxljob-tool - 2.0.0 + 2.1.0 8 @@ -78,7 +78,7 @@ 1.5.1 false - true + diff --git a/xxl-job/src/main/java/com/netease/lowcode/xxljob/task/InterfaceJobHandler.java b/xxl-job/src/main/java/com/netease/lowcode/xxljob/task/InterfaceJobHandler.java index d998fc2ff..d87e097e9 100644 --- a/xxl-job/src/main/java/com/netease/lowcode/xxljob/task/InterfaceJobHandler.java +++ b/xxl-job/src/main/java/com/netease/lowcode/xxljob/task/InterfaceJobHandler.java @@ -1,6 +1,7 @@ package com.netease.lowcode.xxljob.task; import com.netease.lowcode.xxljob.model.JobHandlerInterfaceModel; +import com.netease.lowcode.xxljob.util.NamingUtils; import com.netease.lowcode.xxljob.util.SpringContextUtil; import com.xxl.job.core.context.XxlJobHelper; import com.xxl.job.core.handler.IJobHandler; @@ -56,24 +57,25 @@ public void execute() { return; } Class aClass = AopUtils.getTargetClass(bean); + String logicName = NamingUtils.toLowerCamelCase(model.getLogicName()); try { // 无参 - Method targetMethod = aClass.getDeclaredMethod(model.getLogicName()); + Method targetMethod = aClass.getDeclaredMethod(logicName); targetMethod.invoke(bean); } catch (NoSuchMethodException exception) { try { - Method targetMethod = aClass.getDeclaredMethod(model.getLogicName(), String.class); + Method targetMethod = aClass.getDeclaredMethod(logicName, String.class); targetMethod.invoke(bean, param); } catch (Exception e) { - log.error("Execute job failed: " + handlerName, ((InvocationTargetException) e).getTargetException().getMessage()); - XxlJobHelper.log("任务执行失败: " + ((InvocationTargetException) e).getTargetException().getMessage()); - XxlJobHelper.handleFail("任务执行失败: " + ((InvocationTargetException) e).getTargetException().getMessage()); + log.error("Execute job failed: " + handlerName, e.getCause()); + XxlJobHelper.log("任务执行失败: " + e.getCause()); + XxlJobHelper.handleFail("任务执行失败: " + e.getCause()); return; } } catch (Exception e) { log.error("Execute job failed: " + handlerName, e); - XxlJobHelper.log("任务执行失败: " + ((InvocationTargetException) e).getTargetException().getMessage()); - XxlJobHelper.handleFail("任务执行失败: " + ((InvocationTargetException) e).getTargetException().getMessage()); + XxlJobHelper.log("任务执行失败: " + e.getCause()); + XxlJobHelper.handleFail("任务执行失败: " + e.getCause()); return; } } diff --git a/xxl-job/src/main/java/com/netease/lowcode/xxljob/util/NamingUtils.java b/xxl-job/src/main/java/com/netease/lowcode/xxljob/util/NamingUtils.java new file mode 100644 index 000000000..ded3fc226 --- /dev/null +++ b/xxl-job/src/main/java/com/netease/lowcode/xxljob/util/NamingUtils.java @@ -0,0 +1,36 @@ +package com.netease.lowcode.xxljob.util; + +public class NamingUtils { + public static String toLowerCamelCase(String str) { + if (str == null || str.isEmpty()) { + return str; + } + + // 如果已经是首字母小写,直接返回 + if (Character.isLowerCase(str.charAt(0))) { + return str; + } + + // 处理多单词的驼峰命名 + StringBuilder result = new StringBuilder(); + boolean firstWord = true; + + // 分割单词(根据大写字母分割) + for (int i = 0; i < str.length(); i++) { + char c = str.charAt(i); + if (Character.isUpperCase(c) && i > 0) { + if (firstWord) { + result.append(Character.toLowerCase(c)); + firstWord = false; + } else { + result.append(c); + } + } else { + result.append(firstWord ? Character.toLowerCase(c) : c); + firstWord = false; + } + } + + return result.toString(); + } +}