Repository: draco1023/poi-tl-ext Branch: main Commit: 9bb03841fcd3 Files: 114 Total size: 616.6 KB Directory structure: gitextract_tlgs8gmd/ ├── .gitattributes ├── .github/ │ └── ISSUE_TEMPLATE/ │ └── bug_report.md ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── gradle/ │ └── wrapper/ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src/ ├── main/ │ ├── java/ │ │ └── org/ │ │ ├── apache/ │ │ │ └── poi/ │ │ │ └── xwpf/ │ │ │ └── usermodel/ │ │ │ ├── SVGPictureData.java │ │ │ └── SVGRelation.java │ │ ├── ddr/ │ │ │ ├── image/ │ │ │ │ ├── ImageInfo.java │ │ │ │ ├── ImageInputStreamWrapper.java │ │ │ │ ├── ImageType.java │ │ │ │ ├── MetadataReader.java │ │ │ │ ├── MetadataReaders.java │ │ │ │ ├── avif/ │ │ │ │ │ ├── AvifImageReader.java │ │ │ │ │ ├── AvifImageReaderSpi.java │ │ │ │ │ ├── AvifMetadataReader.java │ │ │ │ │ └── AvifProviderInfo.java │ │ │ │ ├── bmp/ │ │ │ │ │ └── BmpMetadataReader.java │ │ │ │ ├── eps/ │ │ │ │ │ └── EpsMetadataReader.java │ │ │ │ ├── gif/ │ │ │ │ │ └── GifMetadataReader.java │ │ │ │ ├── heif/ │ │ │ │ │ ├── HeifImageReader.java │ │ │ │ │ ├── HeifImageReaderSpi.java │ │ │ │ │ ├── HeifMetadataReader.java │ │ │ │ │ └── HeifProviderInfo.java │ │ │ │ ├── jpeg/ │ │ │ │ │ └── JpegMetadataReader.java │ │ │ │ ├── png/ │ │ │ │ │ └── PngMetadataReader.java │ │ │ │ ├── tiff/ │ │ │ │ │ └── TiffMetadataReader.java │ │ │ │ └── webp/ │ │ │ │ └── WebpMetadataReader.java │ │ │ └── poi/ │ │ │ ├── html/ │ │ │ │ ├── ElementRenderer.java │ │ │ │ ├── ElementRendererProvider.java │ │ │ │ ├── HtmlConstants.java │ │ │ │ ├── HtmlRenderConfig.java │ │ │ │ ├── HtmlRenderContext.java │ │ │ │ ├── HtmlRenderPolicy.java │ │ │ │ ├── tag/ │ │ │ │ │ ├── ARenderer.java │ │ │ │ │ ├── BigRenderer.java │ │ │ │ │ ├── BoldRenderer.java │ │ │ │ │ ├── BreakRenderer.java │ │ │ │ │ ├── DeleteRenderer.java │ │ │ │ │ ├── FigureCaptionRenderer.java │ │ │ │ │ ├── FigureRenderer.java │ │ │ │ │ ├── HeaderBreakRenderer.java │ │ │ │ │ ├── HeaderRenderer.java │ │ │ │ │ ├── ImageRenderer.java │ │ │ │ │ ├── ItalicRenderer.java │ │ │ │ │ ├── LaTeXRenderer.java │ │ │ │ │ ├── ListItemRenderer.java │ │ │ │ │ ├── ListRenderer.java │ │ │ │ │ ├── MarkRenderer.java │ │ │ │ │ ├── MathRenderer.java │ │ │ │ │ ├── OmittedRenderer.java │ │ │ │ │ ├── PreRenderer.java │ │ │ │ │ ├── RubyRenderer.java │ │ │ │ │ ├── SmallRenderer.java │ │ │ │ │ ├── SubscriptRenderer.java │ │ │ │ │ ├── SuperscriptRenderer.java │ │ │ │ │ ├── SvgRenderer.java │ │ │ │ │ ├── TableCellRenderer.java │ │ │ │ │ ├── TableRenderer.java │ │ │ │ │ ├── UnderlineRenderer.java │ │ │ │ │ └── WalkThroughRenderer.java │ │ │ │ └── util/ │ │ │ │ ├── BoxProperty.java │ │ │ │ ├── CSSLength.java │ │ │ │ ├── CSSLengthUnit.java │ │ │ │ ├── CSSStyleUtils.java │ │ │ │ ├── Colors.java │ │ │ │ ├── ColumnStyle.java │ │ │ │ ├── EmptyCSSStyle.java │ │ │ │ ├── InlineStyle.java │ │ │ │ ├── JsoupUtils.java │ │ │ │ ├── ListStyle.java │ │ │ │ ├── ListStyleType.java │ │ │ │ ├── NamedBorderWidth.java │ │ │ │ ├── NamedFontSize.java │ │ │ │ ├── NumberingContext.java │ │ │ │ ├── RenderUtils.java │ │ │ │ ├── Span.java │ │ │ │ ├── SpanWidth.java │ │ │ │ ├── WhiteSpaceRule.java │ │ │ │ └── XWPFParagraphRuns.java │ │ │ ├── latex/ │ │ │ │ ├── LaTeXRenderPolicy.java │ │ │ │ ├── LaTeXUtils.java │ │ │ │ ├── TagHandler.java │ │ │ │ └── TextCircledHandler.java │ │ │ ├── math/ │ │ │ │ ├── EmptyEOfNaryDisplayMode.java │ │ │ │ ├── MathMLRenderPolicy.java │ │ │ │ ├── MathMLUtils.java │ │ │ │ └── MathRenderConfig.java │ │ │ └── util/ │ │ │ ├── ByteArrayCopyStream.java │ │ │ ├── HttpURLConnectionUtils.java │ │ │ └── XmlUtils.java │ │ └── jsoup/ │ │ └── parser/ │ │ └── CustomHtmlTreeBuilder.java │ └── resources/ │ ├── META-INF/ │ │ └── services/ │ │ └── javax.imageio.spi.ImageReaderSpi │ ├── MML2OMML.XSL │ ├── math-character-aliases.txt │ └── math-character-circled.txt └── test/ ├── java/ │ └── org/ │ └── ddr/ │ └── poi/ │ ├── FileReader.java │ ├── html/ │ │ └── HtmlRenderPolicyTest.java │ ├── latex/ │ │ └── LaTeXRenderPolicyTest.java │ └── math/ │ └── MathMLRenderPolicyTest.java └── resources/ ├── 0.xml ├── 1.html ├── 1.xml ├── 2.html ├── 2.xml ├── 3.xml ├── math.docx ├── notes.docx ├── poi.docx └── simplelogger.properties ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ # # https://help.github.com/articles/dealing-with-line-endings/ # # These are explicitly windows files and should use crlf *.bat text eol=crlf ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **HTML content:**

Some html content to reproduce the issue

**Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **poi-tl-ext version:** [e.g. 0.3.12] **poi-tl version:** [e.g. 1.9.1] **Additional context** Add any other context about the problem here. ================================================ FILE: .gitignore ================================================ # Compiled class file *.class # Log file *.log # BlueJ files *.ctxt # Mobile Tools for Java (J2ME) .mtj.tmp/ # Package Files # *.jar *.war *.nar *.ear *.zip *.tar.gz *.rar /*.docx ### STS ### .apt_generated .classpath .factorypath .project .settings .springBeans ### IntelliJ IDEA ### .idea/ *.iws *.iml *.ipr ### NetBeans ### nbproject/private/ !dev/build/* build/* nbbuild/ dist/ nbdist/ .nb-gradle/ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* # Ignore Gradle project-specific cache directory .gradle/ gradle.properties # Ignore Gradle build output directory build/ out/ .gradletasknamecache secring.gpg ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # poi-tl-ext ![GitHub](https://img.shields.io/github/license/draco1023/poi-tl-ext) ![JDK](https://img.shields.io/badge/jdk-1.8-blue) # Maven poi 4.x poi-tl 1.11 以前的版本 ```xml io.github.draco1023 poi-tl-ext 0.4.26 ``` poi 5.x poi-tl 1.11.0+ ```xml io.github.draco1023 poi-tl-ext 0.4.26-poi5 ``` # 扩展功能 在 [poi-tl](https://github.com/Sayi/poi-tl) 的基础上扩展了如下功能: - 支持渲染`HTML`字符串,插件`HtmlRenderPolicy`的使用方法如下(也可参考[文档](http://deepoove.com/poi-tl/#_%E4%BD%BF%E7%94%A8%E6%8F%92%E4%BB%B6)) ```java HtmlRenderPolicy htmlRenderPolicy = new HtmlRenderPolicy(); Configure configure = Configure.builder() .bind("key", htmlRenderPolicy) .build(); Map data = new HashMap<>(); data.put("key", "

Hello world!

"); XWPFTemplate.compile("input.docx", configure).render(data).writeToFile("output.docx"); ``` `HtmlRenderPolicy`可以通过`HtmlRenderConfig`进行如下设置: - `globalFont` 全局默认字体(用于归一化处理,而不是用于样式兜底) - `globalFontSize` 全局默认字号(用于归一化处理,而不是用于样式兜底) - `showDefaultTableBorderInTableCell` 是否显示嵌套表格的边框(`poi`生成嵌套表格时默认不显示边框,见[#12](https://github.com/draco1023/poi-tl-ext/issues/12)) - `numberingIndent` 多级列表项缩进长度,默认值360 - `numberingHanging` 列表项悬挂长度,默认值360,CSS样式`list-style-position`为`inside`时该参数无效 - `numberingSpacing` 列表编号与内容之间的间隔类型,`STLevelSuffix.NOTHING`/`STLevelSuffix.SPACE`/`STLevelSuffix.TAB` 自定义``标签,允许渲染嵌入在`HTML`中的`LaTeX`,字符串格式可参考[文档](https://www2.ph.ed.ac.uk/snuggletex/documentation/supported-latex.html)。 _目前实现了富文本编辑器可实现的大部分效果,后续继续改进..._ - 支持渲染`MathML`字符串,插件类为`MathMLRenderPolicy` - 支持渲染`LaTeX`字符串,插件类为`LaTeXRenderPolicy` ## 支持我 如果您觉得这个插件节省了您的时间和精力,或者解决了您的难题,可以考虑支持一下我的工作,感谢! ⚡⚡⚡ ![wechat_sp](https://pub-1085551b511e4719a277177fe3c8f95b.r2.dev/wechat_sp.jpg) ================================================ FILE: build.gradle ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ plugins { id 'java-library' id 'io.github.sgtsilvio.gradle.maven-central-publishing' version '0.4.0' } def getBranchName() { providers.exec { commandLine("git", "branch", "--show-current") }.standardOutput.asText.get().trim() } ext { VERSION_NAME = 'poi-5' == getBranchName()? "${project.V}-poi5" : project.V } group = GROUP version = VERSION_NAME repositories { mavenLocal() jcenter() mavenCentral() } dependencies { api 'com.deepoove:poi-tl:1.9.1' api 'org.apache.poi:ooxml-schemas:1.4' api 'org.apache.commons:commons-lang3:3.10' api 'commons-io:commons-io:2.11.0' implementation 'net.sourceforge.cssparser:cssparser:0.9.29' implementation 'org.jsoup:jsoup:1.15.3' implementation 'net.sf.saxon:Saxon-HE:11.4' implementation 'de.rototor.snuggletex:snuggletex-core:1.3.0' implementation 'com.drewnoakes:metadata-extractor:2.19.0' implementation 'com.twelvemonkeys.imageio:imageio-batik:3.10.1' implementation 'org.apache.xmlgraphics:batik-rasterizer-ext:1.17' implementation 'com.twelvemonkeys.imageio:imageio-webp:3.10.1' implementation 'com.twelvemonkeys.imageio:imageio-pict:3.10.1' implementation 'com.twelvemonkeys.imageio:imageio-tiff:3.10.1' implementation 'com.twelvemonkeys.imageio:imageio-jpeg:3.10.1' implementation 'com.twelvemonkeys.imageio:imageio-bmp:3.10.1' compileOnly 'com.google.code.findbugs:jsr305:3.0.2' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.2' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.6.2' testRuntimeOnly 'org.slf4j:slf4j-simple:1.7.7' } sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 test { useJUnitPlatform() } tasks.withType(JavaCompile) { options.encoding = 'UTF-8' options.fork = true } javadoc { options { encoding 'UTF-8' charSet 'UTF-8' } } javadoc.options.addBooleanOption('Xdoclint:none', true) java { withSourcesJar() withJavadocJar() } publishing { publications { maven(MavenPublication) { from components.java groupId = GROUP artifactId = POM_ARTIFACT_ID version = VERSION_NAME pom { name = POM_NAME description = POM_DESCRIPTION url = POM_URL licenses { license { name = POM_LICENCE_NAME url = POM_LICENCE_URL } } developers { developer { id = POM_DEVELOPER_ID name = POM_DEVELOPER_NAME url = POM_DEVELOPER_URL } } scm { connection = POM_SCM_CONNECTION developerConnection = POM_SCM_DEV_CONNECTION url = POM_SCM_URL } } } } } signing { sign publishing.publications.maven } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ # # Copyright 2016 - 2021 Draco, https://github.com/draco1023 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: gradlew ================================================ #!/usr/bin/env sh # # Copyright 2015 the original author or authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Dfile.encoding=UTF-8" "-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn () { echo "$*" } die () { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin or MSYS, switch paths to Windows format before running java if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=`expr $i + 1` done case $i in 0) set -- ;; 1) set -- "$args0" ;; 2) set -- "$args0" "$args1" ;; 3) set -- "$args0" "$args1" "$args2" ;; 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Escape application args save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } APP_ARGS=`save "$@"` # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Dfile.encoding=UTF-8" "-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: settings.gradle ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ rootProject.name = 'poi-tl-ext' ================================================ FILE: src/main/java/org/apache/poi/xwpf/usermodel/SVGPictureData.java ================================================ package org.apache.poi.xwpf.usermodel; import org.apache.poi.openxml4j.opc.PackagePart; /** * SVGPictureData * * @author Draco * @since 2022-04-12 */ public class SVGPictureData extends XWPFPictureData { public static final int PICTURE_TYPE_SVG = 1; public SVGPictureData() { } public SVGPictureData(PackagePart part) { super(part); } public static void initRelation() { RELATIONS[PICTURE_TYPE_SVG] = SVGRelation.INSTANCE; } } ================================================ FILE: src/main/java/org/apache/poi/xwpf/usermodel/SVGRelation.java ================================================ package org.apache.poi.xwpf.usermodel; import org.apache.poi.ooxml.POIXMLRelation; import org.apache.poi.openxml4j.opc.PackageRelationshipTypes; import org.apache.poi.sl.usermodel.PictureData; import javax.xml.namespace.QName; /** * SVGRelation * * @author Draco * @since 2022-04-12 */ public class SVGRelation extends POIXMLRelation { public static final SVGRelation INSTANCE = new SVGRelation(); /** * @see org.apache.poi.xslf.usermodel.XSLFPictureShape#MS_SVG_NS */ public static final String MS_SVG_NS = "http://schemas.microsoft.com/office/drawing/2016/SVG/main"; /** * @see org.apache.poi.xslf.usermodel.XSLFPictureShape#SVG_URI */ public static final String SVG_URI = "{96DAC541-7B7A-43D3-8B79-37D633B846F1}"; /** * @see org.apache.poi.xslf.usermodel.XSLFPictureShape#EMBED_TAG */ public static final QName EMBED_TAG = new QName(PackageRelationshipTypes.CORE_PROPERTIES_ECMA376_NS, "embed", "r"); public static final String SVG_BLIP = "svgBlip"; public static final String SVG_PREFIX = "asvg"; public static final QName SVG_QNAME = new QName(MS_SVG_NS, SVG_BLIP, SVG_PREFIX); /** * @see XWPFRelation#IMAGE_PNG */ private SVGRelation() { super(PictureData.PictureType.SVG.contentType, PackageRelationshipTypes.IMAGE_PART, "/word/media/image#.svg", SVGPictureData::new, SVGPictureData::new, null); } } ================================================ FILE: src/main/java/org/ddr/image/ImageInfo.java ================================================ package org.ddr.image; import java.awt.*; import java.io.ByteArrayInputStream; public class ImageInfo { private ByteArrayInputStream stream; private ImageType type; private Dimension dimension; public ImageInfo(ByteArrayInputStream stream) { this.stream = stream; } public ImageInfo(ByteArrayInputStream stream, ImageType type, Dimension dimension) { this.stream = stream; this.type = type; this.dimension = dimension; } public ByteArrayInputStream getStream() { return stream; } public void setStream(ByteArrayInputStream stream) { this.stream = stream; } public ImageType getType() { return type; } public void setType(ImageType type) { this.type = type; } public Dimension getDimension() { return dimension; } public void setDimension(Dimension dimension) { this.dimension = dimension; } public int getWidth() { return dimension == null ? 0 : dimension.width; } public int getHeight() { return dimension == null ? 0 : dimension.height; } public int getRawType() { return type == null ? -1 : type.getType(); } } ================================================ FILE: src/main/java/org/ddr/image/ImageInputStreamWrapper.java ================================================ package org.ddr.image; import javax.imageio.stream.IIOByteBuffer; import javax.imageio.stream.ImageInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.ByteOrder; public class ImageInputStreamWrapper extends InputStream implements ImageInputStream { private final ImageInputStream input; public ImageInputStreamWrapper(ImageInputStream input) { this.input = input; } @Override public void setByteOrder(ByteOrder byteOrder) { input.setByteOrder(byteOrder); } @Override public ByteOrder getByteOrder() { return input.getByteOrder(); } @Override public int read() throws IOException { return input.read(); } @Override public void readBytes(IIOByteBuffer buf, int len) throws IOException { input.readBytes(buf, len); } @Override public boolean readBoolean() throws IOException { return input.readBoolean(); } @Override public byte readByte() throws IOException { return input.readByte(); } @Override public int readUnsignedByte() throws IOException { return input.readUnsignedByte(); } @Override public short readShort() throws IOException { return input.readShort(); } @Override public int readUnsignedShort() throws IOException { return input.readUnsignedShort(); } @Override public char readChar() throws IOException { return input.readChar(); } @Override public int readInt() throws IOException { return input.readInt(); } @Override public long readUnsignedInt() throws IOException { return input.readUnsignedInt(); } @Override public long readLong() throws IOException { return input.readLong(); } @Override public float readFloat() throws IOException { return input.readFloat(); } @Override public double readDouble() throws IOException { return input.readDouble(); } @Override public String readLine() throws IOException { return input.readLine(); } @Override public String readUTF() throws IOException { return input.readUTF(); } @Override public void readFully(byte[] b, int off, int len) throws IOException { input.readFully(b, off, len); } @Override public void readFully(byte[] b) throws IOException { input.readFully(b); } @Override public void readFully(short[] s, int off, int len) throws IOException { input.readFully(s, off, len); } @Override public void readFully(char[] c, int off, int len) throws IOException { input.readFully(c, off, len); } @Override public void readFully(int[] i, int off, int len) throws IOException { input.readFully(i, off, len); } @Override public void readFully(long[] l, int off, int len) throws IOException { input.readFully(l, off, len); } @Override public void readFully(float[] f, int off, int len) throws IOException { input.readFully(f, off, len); } @Override public void readFully(double[] d, int off, int len) throws IOException { input.readFully(d, off, len); } @Override public long getStreamPosition() throws IOException { return input.getStreamPosition(); } @Override public int getBitOffset() throws IOException { return input.getBitOffset(); } @Override public void setBitOffset(int bitOffset) throws IOException { input.setBitOffset(bitOffset); } @Override public int readBit() throws IOException { return input.readBit(); } @Override public long readBits(int numBits) throws IOException { return input.readBits(numBits); } @Override public long length() throws IOException { return input.length(); } @Override public int skipBytes(int n) throws IOException { return input.skipBytes(n); } @Override public long skipBytes(long n) throws IOException { return input.skipBytes(n); } @Override public void seek(long pos) throws IOException { input.seek(pos); } @Override public void mark() { input.mark(); } @Override public void flushBefore(long pos) throws IOException { input.flushBefore(pos); } @Override public void flush() throws IOException { input.flush(); } @Override public long getFlushedPosition() { return input.getFlushedPosition(); } @Override public boolean isCached() { return input.isCached(); } @Override public boolean isCachedMemory() { return input.isCachedMemory(); } @Override public boolean isCachedFile() { return input.isCachedFile(); } } ================================================ FILE: src/main/java/org/ddr/image/ImageType.java ================================================ package org.ddr.image; import org.apache.poi.xwpf.usermodel.Document; public enum ImageType { EMF(Document.PICTURE_TYPE_EMF), WMF(Document.PICTURE_TYPE_WMF), PICT(Document.PICTURE_TYPE_PICT), JPEG(Document.PICTURE_TYPE_JPEG), JPG(Document.PICTURE_TYPE_JPEG), PNG(Document.PICTURE_TYPE_PNG), DIB(Document.PICTURE_TYPE_DIB), GIF(Document.PICTURE_TYPE_GIF), TIF(Document.PICTURE_TYPE_TIFF), TIFF(Document.PICTURE_TYPE_TIFF), EPS(Document.PICTURE_TYPE_EPS), BMP(Document.PICTURE_TYPE_BMP), WPG(Document.PICTURE_TYPE_WPG); private final int type; ImageType(int type) { this.type = type; } public String getExtension() { return name().toLowerCase(); } public int getType() { return type; } } ================================================ FILE: src/main/java/org/ddr/image/MetadataReader.java ================================================ package org.ddr.image; import com.drew.imaging.FileType; import com.drew.metadata.Metadata; import java.awt.*; public interface MetadataReader { boolean canRead(FileType type); ImageType getType(Metadata metadata); Dimension getDimension(Metadata metadata); } ================================================ FILE: src/main/java/org/ddr/image/MetadataReaders.java ================================================ package org.ddr.image; import org.ddr.image.avif.AvifMetadataReader; import org.ddr.image.bmp.BmpMetadataReader; import org.ddr.image.eps.EpsMetadataReader; import org.ddr.image.gif.GifMetadataReader; import org.ddr.image.heif.HeifMetadataReader; import org.ddr.image.jpeg.JpegMetadataReader; import org.ddr.image.png.PngMetadataReader; import org.ddr.image.tiff.TiffMetadataReader; import org.ddr.image.webp.WebpMetadataReader; public class MetadataReaders { public static final MetadataReader[] INSTANCES = { new JpegMetadataReader(), new PngMetadataReader(), new GifMetadataReader(), new WebpMetadataReader(), new AvifMetadataReader(), new HeifMetadataReader(), new BmpMetadataReader(), new TiffMetadataReader(), new EpsMetadataReader() }; } ================================================ FILE: src/main/java/org/ddr/image/avif/AvifImageReader.java ================================================ package org.ddr.image.avif; import org.ddr.image.heif.HeifImageReader; import javax.imageio.spi.ImageReaderSpi; public class AvifImageReader extends HeifImageReader { public AvifImageReader(ImageReaderSpi provider) { super(provider, new AvifMetadataReader(), "avif"); } } ================================================ FILE: src/main/java/org/ddr/image/avif/AvifImageReaderSpi.java ================================================ package org.ddr.image.avif; import com.twelvemonkeys.imageio.spi.ImageReaderSpiBase; import javax.imageio.ImageReader; import javax.imageio.stream.ImageInputStream; import java.io.IOException; import java.util.HashSet; import java.util.Locale; import java.util.Set; public final class AvifImageReaderSpi extends ImageReaderSpiBase { private static final Set TYPES; static { TYPES = new HashSet<>(4); // TYPES.add("mif1"); // TYPES.add("msf1"); TYPES.add("miaf"); TYPES.add("avif"); TYPES.add("avis"); TYPES.add("avio"); } public AvifImageReaderSpi() { super(new AvifProviderInfo()); } @Override public boolean canDecodeInput(Object source) throws IOException { return source instanceof ImageInputStream && canDecode((ImageInputStream) source); } private boolean canDecode(ImageInputStream input) throws IOException { try { input.mark(); for (int i = 0; i < 4; i++) { input.read(); } if (input.read() == 'f' && input.read() == 't' && input.read() == 'y' && input.read() == 'p') { byte[] bytes = new byte[4]; int length = input.read(bytes); if (length == 4) { String s = new String(bytes); return TYPES.contains(s); } } } catch (Exception ignored) { } finally { input.reset(); } return false; } @Override public ImageReader createReaderInstance(Object extension) throws IOException { return new AvifImageReader(this); } @Override public String getDescription(Locale locale) { return "AV1 Image File (AVIF) format image reader"; } } ================================================ FILE: src/main/java/org/ddr/image/avif/AvifMetadataReader.java ================================================ package org.ddr.image.avif; import com.drew.imaging.FileType; import org.ddr.image.heif.HeifMetadataReader; public class AvifMetadataReader extends HeifMetadataReader { @Override public boolean canRead(FileType type) { // FIXME metadata-extractor 一直未发版支持 AVIF 格式,会被归为 QuickTime 格式 return type == FileType.QuickTime || type == FileType.Heif; } } ================================================ FILE: src/main/java/org/ddr/image/avif/AvifProviderInfo.java ================================================ package org.ddr.image.avif; import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo; final class AvifProviderInfo extends ReaderWriterProviderInfo { AvifProviderInfo() { super( AvifProviderInfo.class, new String[]{"avif", "AVIF"}, // Names new String[]{"avif", "avifs"}, // Suffixes new String[]{"image/avif", "image/avifs"}, // Mime-types "org.ddr.image.avif.AvifImageReader", // Reader class name new String[]{"org.ddr.image.avif.AvifImageReaderSpi"}, null, null, false, null, null, null, null, true, null, null, null, null ); } } ================================================ FILE: src/main/java/org/ddr/image/bmp/BmpMetadataReader.java ================================================ package org.ddr.image.bmp; import com.drew.imaging.FileType; import com.drew.metadata.Directory; import com.drew.metadata.Metadata; import com.drew.metadata.bmp.BmpHeaderDirectory; import org.ddr.image.ImageType; import org.ddr.image.MetadataReader; import java.awt.*; public class BmpMetadataReader implements MetadataReader { @Override public boolean canRead(FileType type) { return type == FileType.Bmp; } @Override public ImageType getType(Metadata metadata) { return ImageType.BMP; } @Override public Dimension getDimension(Metadata metadata) { for (Directory directory : metadata.getDirectories()) { if (directory instanceof BmpHeaderDirectory) { Integer width = directory.getInteger(BmpHeaderDirectory.TAG_IMAGE_WIDTH); Integer height = directory.getInteger(BmpHeaderDirectory.TAG_IMAGE_HEIGHT); if (width != null && height != null) { return new Dimension(width, height); } } } return null; } } ================================================ FILE: src/main/java/org/ddr/image/eps/EpsMetadataReader.java ================================================ package org.ddr.image.eps; import com.drew.imaging.FileType; import com.drew.metadata.Directory; import com.drew.metadata.Metadata; import com.drew.metadata.eps.EpsDirectory; import org.ddr.image.ImageType; import org.ddr.image.MetadataReader; import java.awt.*; public class EpsMetadataReader implements MetadataReader{ @Override public boolean canRead(FileType type) { return type == FileType.Eps; } @Override public ImageType getType(Metadata metadata) { return ImageType.EPS; } @Override public Dimension getDimension(Metadata metadata) { for (Directory directory : metadata.getDirectories()) { if (directory instanceof EpsDirectory) { Integer width = directory.getInteger(EpsDirectory.TAG_IMAGE_WIDTH); Integer height = directory.getInteger(EpsDirectory.TAG_IMAGE_HEIGHT); if (width != null && height != null) { return new Dimension(width, height); } } } return null; } } ================================================ FILE: src/main/java/org/ddr/image/gif/GifMetadataReader.java ================================================ package org.ddr.image.gif; import com.drew.imaging.FileType; import com.drew.metadata.Directory; import com.drew.metadata.Metadata; import com.drew.metadata.gif.GifHeaderDirectory; import org.ddr.image.ImageType; import org.ddr.image.MetadataReader; import java.awt.*; public class GifMetadataReader implements MetadataReader{ @Override public boolean canRead(FileType type) { return type == FileType.Gif; } @Override public ImageType getType(Metadata metadata) { return ImageType.GIF; } @Override public Dimension getDimension(Metadata metadata) { for (Directory directory : metadata.getDirectories()) { if (directory instanceof GifHeaderDirectory) { Integer width = directory.getInteger(GifHeaderDirectory.TAG_IMAGE_WIDTH); Integer height = directory.getInteger(GifHeaderDirectory.TAG_IMAGE_HEIGHT); if (width != null && height != null) { return new Dimension(width, height); } } } return null; } } ================================================ FILE: src/main/java/org/ddr/image/heif/HeifImageReader.java ================================================ package org.ddr.image.heif; import com.drew.imaging.FileType; import com.drew.imaging.ImageMetadataReader; import com.drew.imaging.ImageProcessingException; import com.drew.metadata.Metadata; import com.twelvemonkeys.imageio.ImageReaderBase; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.ddr.image.ImageInputStreamWrapper; import org.ddr.image.MetadataReader; import org.ddr.poi.util.HttpURLConnectionUtils; import org.jsoup.Jsoup; import org.jsoup.nodes.Element; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.imageio.ImageIO; import javax.imageio.ImageReadParam; import javax.imageio.ImageTypeSpecifier; import javax.imageio.spi.ImageReaderSpi; import java.awt.*; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; import java.nio.charset.StandardCharsets; import java.util.Iterator; public class HeifImageReader extends ImageReaderBase { private static final Logger log = LoggerFactory.getLogger(HeifImageReader.class); protected final MetadataReader metadataReader; protected ImageInputStreamWrapper wrapper; protected Metadata metadata; protected Dimension dimension; protected String format; public HeifImageReader(ImageReaderSpi provider) { this(provider, new HeifMetadataReader(), "heic"); } protected HeifImageReader(ImageReaderSpi provider, MetadataReader metadataReader, String format) { super(provider); this.metadataReader = metadataReader; this.format = format; } @Override protected void resetMembers() { metadata = null; dimension = null; } @Override public void setInput(Object input, boolean seekForwardOnly, boolean ignoreMetadata) { super.setInput(input, seekForwardOnly, ignoreMetadata); if (imageInput != null) { wrapper = new ImageInputStreamWrapper(imageInput); try { metadata = ImageMetadataReader.readMetadata(wrapper, 0, FileType.Heif); dimension = metadataReader.getDimension(metadata); } catch (IOException | ImageProcessingException e) { log.warn("Failed to read metadata", e); } } } @Override public int getWidth(int imageIndex) throws IOException { return dimension == null ? 0 : dimension.width; } @Override public int getHeight(int imageIndex) throws IOException { return dimension == null ? 0 : dimension.height; } @Override public Iterator getImageTypes(int imageIndex) throws IOException { return null; } @Override public BufferedImage read(int imageIndex, ImageReadParam param) throws IOException { return convert(param); } BufferedImage convert(ImageReadParam param) { HttpURLConnection uploadConnection = null; HttpURLConnection convertConnection = null; HttpURLConnection downloadConnection = null; try { uploadConnection = HttpURLConnectionUtils.connect("https://ezgif.com/" + format + "-to-jpg"); uploadConnection.setInstanceFollowRedirects(false); HttpURLConnectionUtils.initUserAgent(uploadConnection); uploadConnection.setRequestProperty("Referer", "https://ezgif.com/" + format + "-to-jpg"); String boundary = HttpURLConnectionUtils.initFormData(uploadConnection); try (OutputStream outputStream = uploadConnection.getOutputStream()) { byte[] boundaryBytes = ("--" + boundary).getBytes(); wrapper.seek(0); HttpURLConnectionUtils.addFormData(outputStream, boundaryBytes, "new-image", "some." + format, wrapper); outputStream.write(boundaryBytes); outputStream.write("--".getBytes()); outputStream.write(HttpURLConnectionUtils.newLineBytes); outputStream.flush(); } // 获取上传响应 int uploadResponseCode = uploadConnection.getResponseCode(); if (uploadResponseCode == HttpURLConnection.HTTP_MOVED_TEMP) { String location = uploadConnection.getHeaderField("Location"); String convertUrl = StringUtils.substringBeforeLast(location, "."); String fileId = StringUtils.substringAfterLast(convertUrl, "/"); convertUrl += "?ajax=true"; if (log.isDebugEnabled()) { log.debug("{} uploaded: {}", format, fileId); } convertConnection = HttpURLConnectionUtils.connect(convertUrl); HttpURLConnectionUtils.initUserAgent(convertConnection); convertConnection.setRequestProperty("Referer", location); boundary = HttpURLConnectionUtils.initFormData(convertConnection); try (OutputStream convertOutput = convertConnection.getOutputStream()) { byte[] boundaryBytes = ("--" + boundary).getBytes(); HttpURLConnectionUtils.addFormData(convertOutput, boundaryBytes, "file", fileId, null); HttpURLConnectionUtils.addFormData(convertOutput, boundaryBytes, "percentage", "90", null); HttpURLConnectionUtils.addFormData(convertOutput, boundaryBytes, "percentager", "90", null); HttpURLConnectionUtils.addFormData(convertOutput, boundaryBytes, "background", "#ffffff", null); HttpURLConnectionUtils.addFormData(convertOutput, boundaryBytes, "backgroundc", "#ffffff", null); HttpURLConnectionUtils.addFormData(convertOutput, boundaryBytes, "ajax", "true", null); convertOutput.write(boundaryBytes); convertOutput.write("--".getBytes()); convertOutput.write(HttpURLConnectionUtils.newLineBytes); convertOutput.flush(); } int convertResponseCode = convertConnection.getResponseCode(); if (convertResponseCode == HttpURLConnection.HTTP_OK) { try (InputStream convertResponse = convertConnection.getInputStream()) { Element body = Jsoup.parse(convertResponse, StandardCharsets.UTF_8.name(), "").body(); if (log.isDebugEnabled()) { log.debug("{} converted: {}", format, body.html()); } for (Element img : body.select("img")) { String src = img.attr("src"); if (StringUtils.contains(src, "ezgif")) { String url = "https:" + src; downloadConnection = HttpURLConnectionUtils.connect(url); HttpURLConnectionUtils.initUserAgent(downloadConnection); try (InputStream downloadResponse = downloadConnection.getInputStream()) { return ImageIO.read(downloadResponse); } } } } } else { log.warn("Failed to convert {} image. Response code: {}", format, convertResponseCode); } } else { log.warn("Failed to upload image. Response code: {}", uploadResponseCode); } } catch (Exception e) { log.warn("Failed to convert {} image", format, e); IOUtils.close(uploadConnection); IOUtils.close(convertConnection); IOUtils.close(downloadConnection); } return null; } } ================================================ FILE: src/main/java/org/ddr/image/heif/HeifImageReaderSpi.java ================================================ package org.ddr.image.heif; import com.twelvemonkeys.imageio.spi.ImageReaderSpiBase; import javax.imageio.ImageReader; import javax.imageio.stream.ImageInputStream; import java.io.IOException; import java.util.HashSet; import java.util.Locale; import java.util.Set; public final class HeifImageReaderSpi extends ImageReaderSpiBase { private static final Set TYPES; static { TYPES = new HashSet<>(8); TYPES.add("mif1"); TYPES.add("msf1"); TYPES.add("heic"); TYPES.add("heix"); TYPES.add("hevc"); TYPES.add("hevx"); } public HeifImageReaderSpi() { super(new HeifProviderInfo()); } @Override public boolean canDecodeInput(Object source) throws IOException { return source instanceof ImageInputStream && canDecode((ImageInputStream) source); } private boolean canDecode(ImageInputStream input) throws IOException { // https://docs.oracle.com/javase/7/docs/technotes/guides/imageio/spec/extending.fm3.html // https://github.com/strukturag/libheif/blob/e64bb552f5d48fee5daf69c8c2fd59ec3eee0818/libheif/heif.cc#L102 // https://devstreaming-cdn.apple.com/videos/wwdc/2017/513fzgbviu23l/513/513_high_efficiency_image_file_format.pdf?dl=1 try { input.mark(); for (int i = 0; i < 4; i++) { input.read(); } if (input.read() == 'f' && input.read() == 't' && input.read() == 'y' && input.read() == 'p') { byte[] bytes = new byte[4]; int length = input.read(bytes); if (length == 4) { String s = new String(bytes); return TYPES.contains(s); } } } catch (Exception ignored) { } finally { input.reset(); } return false; } @Override public ImageReader createReaderInstance(Object extension) throws IOException { return new HeifImageReader(this); } @Override public String getDescription(Locale locale) { return "High Efficiency Image File (HEIF) format image reader"; } } ================================================ FILE: src/main/java/org/ddr/image/heif/HeifMetadataReader.java ================================================ package org.ddr.image.heif; import com.drew.imaging.FileType; import com.drew.metadata.Directory; import com.drew.metadata.Metadata; import com.drew.metadata.exif.ExifDescriptorBase; import com.drew.metadata.exif.ExifIFD0Directory; import com.drew.metadata.exif.ExifSubIFDDirectory; import com.drew.metadata.heif.HeifDescriptor; import com.drew.metadata.heif.HeifDirectory; import org.ddr.image.ImageType; import org.ddr.image.MetadataReader; import java.awt.*; public class HeifMetadataReader implements MetadataReader { @Override public boolean canRead(FileType type) { return type == FileType.Heif; } @Override public ImageType getType(Metadata metadata) { // FIXME read icc profile return ImageType.JPG; } /** * @see HeifDescriptor#getRotationDescription() * @see ExifDescriptorBase#getOrientationDescription() */ @Override public Dimension getDimension(Metadata metadata) { Integer width = null; Integer height = null; Boolean rotated = null; for (Directory directory : metadata.getDirectories()) { if (directory instanceof HeifDirectory) { Integer r = directory.getInteger(HeifDirectory.TAG_IMAGE_ROTATION); if (r != null) { rotated = r % 2 == 1; } // FIXME 实际上可以在此处获取到 width 和 height,但是图片会直接嵌入到 word 中,能否呈现因系统支持而异,特意不获取走格式转换流程 // if (width == null) { // width = directory.getInteger(HeifDirectory.TAG_IMAGE_WIDTH); // } // if (height == null) { // height = directory.getInteger(HeifDirectory.TAG_IMAGE_HEIGHT); // } } else if (directory instanceof ExifIFD0Directory) { if (rotated == null) { Integer r = directory.getInteger(ExifIFD0Directory.TAG_ORIENTATION); if (r != null) { rotated = r > 4; } } } else if (directory instanceof ExifSubIFDDirectory) { if (width == null) { width = directory.getInteger(ExifSubIFDDirectory.TAG_EXIF_IMAGE_WIDTH); } if (height == null) { height = directory.getInteger(ExifSubIFDDirectory.TAG_EXIF_IMAGE_HEIGHT); } } } if (width != null && height != null) { if (Boolean.TRUE.equals(rotated)) { //noinspection SuspiciousNameCombination return new Dimension(height, width); } return new Dimension(width, height); } return null; } } ================================================ FILE: src/main/java/org/ddr/image/heif/HeifProviderInfo.java ================================================ package org.ddr.image.heif; import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo; final class HeifProviderInfo extends ReaderWriterProviderInfo { HeifProviderInfo() { super( HeifProviderInfo.class, new String[]{"heif", "HEIF"}, // Names new String[]{"heif", "heic"}, // Suffixes new String[]{"image/heif", "image/heic", "image/heif-sequence", "image/heic-sequence"}, // Mime-types "org.ddr.image.heif.HeifImageReader", // Reader class name new String[]{"org.ddr.image.heif.HeifImageReaderSpi"}, null, null, false, null, null, null, null, true, null, null, null, null ); } } ================================================ FILE: src/main/java/org/ddr/image/jpeg/JpegMetadataReader.java ================================================ package org.ddr.image.jpeg; import com.drew.imaging.FileType; import com.drew.metadata.Directory; import com.drew.metadata.Metadata; import com.drew.metadata.jpeg.JpegDirectory; import org.ddr.image.ImageType; import org.ddr.image.MetadataReader; import java.awt.*; public class JpegMetadataReader implements MetadataReader { @Override public boolean canRead(FileType type) { return type == FileType.Jpeg; } @Override public ImageType getType(Metadata metadata) { return ImageType.JPG; } @Override public Dimension getDimension(Metadata metadata) { for (Directory directory : metadata.getDirectories()) { if (directory instanceof JpegDirectory) { Integer width = directory.getInteger(JpegDirectory.TAG_IMAGE_WIDTH); Integer height = directory.getInteger(JpegDirectory.TAG_IMAGE_HEIGHT); if (width != null && height != null) { return new Dimension(width, height); } } } return null; } } ================================================ FILE: src/main/java/org/ddr/image/png/PngMetadataReader.java ================================================ package org.ddr.image.png; import com.drew.imaging.FileType; import com.drew.imaging.png.PngChunkType; import com.drew.metadata.Directory; import com.drew.metadata.Metadata; import com.drew.metadata.png.PngDirectory; import org.ddr.image.ImageType; import org.ddr.image.MetadataReader; import java.awt.*; public class PngMetadataReader implements MetadataReader { @Override public boolean canRead(FileType type) { return type == FileType.Png; } @Override public ImageType getType(Metadata metadata) { return ImageType.PNG; } @Override public Dimension getDimension(Metadata metadata) { for (Directory directory : metadata.getDirectories()) { if (directory instanceof PngDirectory) { if (((PngDirectory) directory).getPngChunkType() == PngChunkType.IHDR) { Integer width = directory.getInteger(PngDirectory.TAG_IMAGE_WIDTH); Integer height = directory.getInteger(PngDirectory.TAG_IMAGE_HEIGHT); if (width != null && height != null) { return new Dimension(width, height); } } } } return null; } } ================================================ FILE: src/main/java/org/ddr/image/tiff/TiffMetadataReader.java ================================================ package org.ddr.image.tiff; import com.drew.imaging.FileType; import com.drew.metadata.Directory; import com.drew.metadata.Metadata; import com.drew.metadata.exif.ExifIFD0Directory; import org.ddr.image.ImageType; import org.ddr.image.MetadataReader; import java.awt.*; import java.util.EnumSet; public class TiffMetadataReader implements MetadataReader { private static final EnumSet TIFF_TYPES = EnumSet.of(FileType.Tiff, FileType.Arw, FileType.Cr2, FileType.Nef, FileType.Orf, FileType.Rw2); @Override public boolean canRead(FileType type) { return TIFF_TYPES.contains(type); } @Override public ImageType getType(Metadata metadata) { return ImageType.TIFF; } @Override public Dimension getDimension(Metadata metadata) { for (Directory directory : metadata.getDirectories()) { if (directory instanceof ExifIFD0Directory) { Integer width = directory.getInteger(ExifIFD0Directory.TAG_IMAGE_WIDTH); Integer height = directory.getInteger(ExifIFD0Directory.TAG_IMAGE_HEIGHT); if (width != null && height != null) { return new Dimension(width, height); } } } return null; } } ================================================ FILE: src/main/java/org/ddr/image/webp/WebpMetadataReader.java ================================================ package org.ddr.image.webp; import com.drew.imaging.FileType; import com.drew.metadata.Directory; import com.drew.metadata.Metadata; import com.drew.metadata.webp.WebpDirectory; import org.ddr.image.ImageType; import org.ddr.image.MetadataReader; import java.awt.*; public class WebpMetadataReader implements MetadataReader{ @Override public boolean canRead(FileType type) { return type == FileType.WebP; } @Override public ImageType getType(Metadata metadata) { for (Directory directory : metadata.getDirectories()) { if (directory instanceof WebpDirectory) { Boolean hasAlpha = directory.getBooleanObject(WebpDirectory.TAG_HAS_ALPHA); if (Boolean.TRUE.equals(hasAlpha)) { return ImageType.PNG; } } } return ImageType.JPG; } @Override public Dimension getDimension(Metadata metadata) { for (Directory directory : metadata.getDirectories()) { if (directory instanceof WebpDirectory) { Integer width = directory.getInteger(WebpDirectory.TAG_IMAGE_WIDTH); Integer height = directory.getInteger(WebpDirectory.TAG_IMAGE_HEIGHT); if (width != null && height != null) { return new Dimension(width, height); } } } return null; } } ================================================ FILE: src/main/java/org/ddr/poi/html/ElementRenderer.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.html; import org.jsoup.nodes.Element; /** * HTML元素渲染器 * * @author Draco * @since 2021-02-08 */ public interface ElementRenderer { /** * 开始渲染 * * @param element HTML元素 * @param context 渲染上下文 * @return 是否继续渲染子元素 */ boolean renderStart(Element element, HtmlRenderContext context); /** * 元素渲染结束需要执行的逻辑 * * @param element HTML元素 * @param context 渲染上下文 */ default void renderEnd(Element element, HtmlRenderContext context) { } /** * @return 支持的HTML标签 */ String[] supportedTags(); /** * @return 是否为块状渲染,如果为true在Word中会另起一个Paragraph */ boolean renderAsBlock(); } ================================================ FILE: src/main/java/org/ddr/poi/html/ElementRendererProvider.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.html; /** * HTML元素渲染器提供者 * * @author Draco * @since 2022-10-21 */ @FunctionalInterface public interface ElementRendererProvider { /** * 根据HTML元素名称获取渲染器 * * @param tagNormalName HTML元素名称(小写) * @return HTML元素渲染器 */ ElementRenderer get(String tagNormalName); } ================================================ FILE: src/main/java/org/ddr/poi/html/HtmlConstants.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.html; import org.apache.commons.compress.utils.Sets; import java.util.Set; /** * HTML常量 * * @author Draco * @since 2021-02-23 */ public interface HtmlConstants { String TAG_A = "a"; String TAG_IMG = "img"; String TAG_BR = "br"; String TAG_MATH = "math"; String TAG_HR = "hr"; String TAG_OL = "ol"; String TAG_UL = "ul"; String TAG_LI = "li"; String TAG_TABLE = "table"; String TAG_S = "s"; String TAG_DEL = "del"; /** * HTML5不支持strike */ String TAG_STRIKE = "strike"; String TAG_I = "i"; String TAG_EM = "em"; String TAG_B = "b"; String TAG_STRONG = "strong"; String TAG_U = "u"; String TAG_MARK = "mark"; String TAG_SUB = "sub"; String TAG_SUP = "sup"; String TAG_H1 = "h1"; String TAG_H2 = "h2"; String TAG_H3 = "h3"; String TAG_H4 = "h4"; String TAG_H5 = "h5"; String TAG_H6 = "h6"; /** * HTML5不支持big */ String TAG_BIG = "big"; String TAG_SMALL = "small"; String TAG_CAPTION = "caption"; String TAG_COLGROUP = "colgroup"; String TAG_COL = "col"; String TAG_TR = "tr"; String TAG_TH = "th"; String TAG_TD = "td"; String TAG_THEAD = "thead"; String TAG_TBODY = "tbody"; String TAG_TFOOT = "tfoot"; String TAG_FRAME = "frame"; String TAG_FRAMESET = "frameset"; String TAG_IFRAME = "iframe"; String TAG_NOFRAMES = "noframes"; String TAG_HTML = "html"; String TAG_HEAD = "head"; String TAG_BODY = "body"; String TAG_SCRIPT = "script"; String TAG_NOSCRIPT = "noscript"; String TAG_TEMPLATE = "template"; String TAG_SVG = "svg"; String TAG_RUBY = "ruby"; String TAG_RP = "rp"; String TAG_RT = "rt"; String TAG_FIGURE = "figure"; String TAG_FIGURE_CAPTION = "figcaption"; String TAG_PRE = "pre"; String TAG_XMP = "xmp"; String TAG_LATEX = "latex"; String ATTR_STYLE = "style"; String ATTR_SRC = "src"; String ATTR_WIDTH = "width"; String ATTR_HEIGHT = "height"; String ATTR_SPAN = "span"; String ATTR_ROWSPAN = "rowspan"; String ATTR_COLSPAN = "colspan"; String ATTR_HREF = "href"; String ATTR_TYPE = "type"; String ATTR_FRAME = "frame"; String ATTR_RULES = "rules"; /** * 自定义属性:行索引 */ String ATTR_ROW_INDEX = "_r"; /** * 自定义属性:列索引 */ String ATTR_COLUMN_INDEX = "_c"; String CSS_BACKGROUND = "background"; String CSS_BACKGROUND_COLOR = "background-color"; String CSS_BORDER = "border"; String CSS_BORDER_STYLE = "border-style"; String CSS_BORDER_WIDTH = "border-width"; String CSS_BORDER_COLOR = "border-color"; String CSS_FONT = "font"; String CSS_MARGIN = "margin"; String CSS_MARGIN_TOP = "margin-top"; String CSS_MARGIN_RIGHT = "margin-right"; String CSS_MARGIN_BOTTOM = "margin-bottom"; String CSS_MARGIN_LEFT = "margin-left"; String CSS_PADDING = "padding"; String CSS_PADDING_TOP = "padding-top"; String CSS_PADDING_RIGHT = "padding-right"; String CSS_PADDING_BOTTOM = "padding-bottom"; String CSS_PADDING_LEFT = "padding-left"; String CSS_FONT_STYLE = "font-style"; String CSS_FONT_VARIANT_CAPS = "font-variant-caps"; String CSS_FONT_WEIGHT = "font-weight"; String CSS_FONT_SIZE = "font-size"; String CSS_LINE_HEIGHT = "line-height"; String CSS_FONT_FAMILY = "font-family"; String CSS_TEXT_DECORATION = "text-decoration"; String CSS_TEXT_DECORATION_LINE = "text-decoration-line"; String CSS_TEXT_DECORATION_STYLE = "text-decoration-style"; String CSS_TEXT_DECORATION_COLOR = "text-decoration-color"; String CSS_TEXT_INDENT = "text-indent"; String CSS_VERTICAL_ALIGN = "vertical-align"; String CSS_VISIBILITY = "visibility"; String CSS_DISPLAY = "display"; String CSS_COLOR = "color"; String CSS_WIDTH = ATTR_WIDTH; String CSS_MAX_WIDTH = "max-width"; String CSS_HEIGHT = ATTR_HEIGHT; String CSS_MAX_HEIGHT = "max-height"; String CSS_BORDER_TOP = "border-top"; String CSS_BORDER_RIGHT = "border-right"; String CSS_BORDER_BOTTOM = "border-bottom"; String CSS_BORDER_LEFT = "border-left"; String CSS_BORDER_TOP_STYLE = "border-top-style"; String CSS_BORDER_RIGHT_STYLE = "border-right-style"; String CSS_BORDER_BOTTOM_STYLE = "border-bottom-style"; String CSS_BORDER_LEFT_STYLE = "border-left-style"; String CSS_BORDER_TOP_WIDTH = "border-top-width"; String CSS_BORDER_RIGHT_WIDTH = "border-right-width"; String CSS_BORDER_BOTTOM_WIDTH = "border-bottom-width"; String CSS_BORDER_LEFT_WIDTH = "border-left-width"; String CSS_BORDER_TOP_COLOR = "border-top-color"; String CSS_BORDER_RIGHT_COLOR = "border-right-color"; String CSS_BORDER_BOTTOM_COLOR = "border-bottom-color"; String CSS_BORDER_LEFT_COLOR = "border-left-color"; String CSS_FLOAT = "float"; String CSS_WHITE_SPACE = "white-space"; String CSS_LIST_STYLE = "list-style"; String CSS_LIST_STYLE_TYPE = "list-style-type"; String CSS_LIST_STYLE_POSITION = "list-style-position"; String CSS_BORDER_COLLAPSE = "border-collapse"; String CSS_BORDER_SPACING = "border-spacing"; String CSS_CAPTION_SIDE = "caption-side"; String CSS_LETTER_SPACING = "letter-spacing"; String CSS_TEXT_ALIGN = "text-align"; String NORMAL = "normal"; String ITALIC = "italic"; String OBLIQUE = "oblique"; String SMALL_CAPS = "small-caps"; String BOLD = "bold"; String BOLDER = "bolder"; String LIGHTER = "lighter"; String START = "start"; String LEFT = "left"; String END = "end"; String RIGHT = "right"; String CENTER = "center"; String JUSTIFY = "justify"; String JUSTIFY_ALL = "justify-all"; String TOP = "top"; String BOTTOM = "bottom"; String MIDDLE = "middle"; String AUTO = "auto"; String XX_SMALL = "xx-small"; String X_SMALL = "x-small"; String SMALL = "small"; String MEDIUM = "medium"; String LARGE = "large"; String X_LARGE = "x-large"; String XX_LARGE = "xx-large"; String XXX_LARGE = "xxx-large"; String SMALLER = "smaller"; String LARGER = "larger"; String THIN = "thin"; String THICK = "thick"; String PT = "pt"; String PC = "pc"; String IN = "in"; String CM = "cm"; String MM = "mm"; String PX = "px"; String EM = "em"; String REM = "rem"; String VW = "vw"; String VH = "vh"; String VMIN = "vmin"; String VMAX = "vmax"; String PERCENT = "%"; // 自定义单位 String EMU = "emu"; /** * dxa的单位,twentieth of a point = 1 / 20 pt */ String TWIP = "twip"; String SLASH = "/"; String COMMA = ","; String COLON = ":"; String SHARP = "#"; String SEMICOLON = ";"; String QUESTION = "?"; String PLUS = "+"; String MINUS = "-"; String LEFT_PARENTHESIS = "("; String LINE_THROUGH = "line-through"; String UNDERLINE = "underline"; String SOLID = "solid"; String DOUBLE = "double"; String DOTTED = "dotted"; String DASHED = "dashed"; String WAVY = "wavy"; String NONE = "none"; String GROOVE = "groove"; String RIDGE = "ridge"; // 类似groove String INSET = "inset"; // 类似ridge String OUTSET = "outset"; String HIDDEN = "hidden"; String COLLAPSE = "collapse"; String SUPER = "super"; String SUB = "sub"; String NO_WRAP = "nowrap"; String PRE = "pre"; String PRE_WRAP = "pre-wrap"; String PRE_LINE = "pre-line"; String BREAK_SPACES = "break-spaces"; String INSIDE = "inside"; String OUTSIDE = "outside"; String VOID = "void"; String ABOVE = "above"; String BELOW = "below"; String H_SIDES = "hsides"; String V_SIDES = "vsides"; String LHS = "lhs"; String RHS = "rhs"; String BOX = "box"; String BORDER = "border"; String GROUPS = "groups"; String ROWS = "rows"; String COLS = "cols"; String ALL = "all"; Set FONT_STYLES = Sets.newHashSet(NORMAL, ITALIC, OBLIQUE); Set FONT_VARIANTS = Sets.newHashSet(NORMAL, SMALL_CAPS); Set FONT_WEIGHTS = Sets.newHashSet(NORMAL, BOLD, BOLDER, LIGHTER); Set BORDER_STYLES = Sets.newHashSet(NONE, HIDDEN, DOTTED, DASHED, SOLID, DOUBLE, GROOVE, RIDGE, INSET, OUTSET); // 不支持overline Set TEXT_DECORATION_LINES = Sets.newHashSet(UNDERLINE, LINE_THROUGH); Set TEXT_DECORATION_STYLES = Sets.newHashSet(SOLID, DOUBLE, DOTTED, DASHED, WAVY); Set LIST_STYLE_POSITIONS = Sets.newHashSet(INSIDE, OUTSIDE); /** * 可继承的样式 * Specification */ Set INHERITABLE_STYLES = Sets.newHashSet( "azimuth", CSS_BORDER_COLLAPSE, CSS_BORDER_SPACING, CSS_CAPTION_SIDE, CSS_COLOR, "cursor", "direction", "elevation", "empty-cells", CSS_FONT_FAMILY, CSS_FONT_SIZE, CSS_FONT_STYLE, CSS_FONT_VARIANT_CAPS, CSS_FONT_WEIGHT, CSS_FONT, CSS_LETTER_SPACING, CSS_LINE_HEIGHT, "list-style-image", "list-style-position", CSS_LIST_STYLE_TYPE, CSS_LIST_STYLE, "orphans", "pitch-range", "pitch", "quotes", "richness", "speak-header", "speak-numeral", "speak-punctuation", "speak", "speech-rate", "stress", CSS_TEXT_ALIGN, CSS_TEXT_INDENT, "text-transform", CSS_VISIBILITY, "voice-family", "volume", CSS_WHITE_SPACE, "widows", "word-spacing" ); /** * 需要保留的空标签 */ Set KEEP_EMPTY_TAGS = Sets.newHashSet(TAG_LI, TAG_HR); /** * Word中一些主要的默认字体 */ Set MAJOR_FONT = Sets.newHashSet("宋体", "SIMSUN", "新細明體", "TIMES NEW ROMAN", "ARIAL"); String DEFINED_ITALIC = inlineStyle(CSS_FONT_STYLE, ITALIC); String DEFINED_STRIKE = inlineStyle(CSS_TEXT_DECORATION_LINE, LINE_THROUGH); String DEFINED_BOLD = inlineStyle(CSS_FONT_WEIGHT, BOLD); String DEFINED_UNDERLINE = inlineStyle(CSS_TEXT_DECORATION_LINE, UNDERLINE); String DEFINED_SUPERSCRIPT = inlineStyle(CSS_VERTICAL_ALIGN, SUPER); String DEFINED_SUBSCRIPT = inlineStyle(CSS_VERTICAL_ALIGN, SUB); String DEFINED_LARGER = inlineStyle(CSS_FONT_SIZE, LARGER); String DEFINED_SMALLER = inlineStyle(CSS_FONT_SIZE, SMALLER); String DEFINED_PRE = inlineStyle(CSS_WHITE_SPACE, PRE); /** * 生成行内样式声明 * * @param key 样式属性 * @param value 样式值 * @return 行内样式声明 */ static String inlineStyle(String key, String value) { return key + COLON + value + SEMICOLON; } /** * @param fontName 字体名称 * @return 是否为主要字体 */ static boolean isMajorFont(String fontName) { return MAJOR_FONT.contains(fontName.toUpperCase()); } } ================================================ FILE: src/main/java/org/ddr/poi/html/HtmlRenderConfig.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.html; import org.ddr.poi.html.util.CSSLength; import org.ddr.poi.math.MathRenderConfig; import org.openxmlformats.schemas.wordprocessingml.x2006.main.STLevelSuffix; import java.util.List; /** * @author Draco * @since 2021-10-26 */ public class HtmlRenderConfig { private String globalFont; private CSSLength globalFontSize; private int globalFontSizeInHalfPoints; private boolean showDefaultTableBorderInTableCell; private List customRenderers; private int numberingIndent = -1; private int numberingHanging = -1; private STLevelSuffix.Enum numberingSpacing; private final MathRenderConfig mathRenderConfig = new MathRenderConfig(); /** * @return global font family */ public String getGlobalFont() { return globalFont; } public void setGlobalFont(String globalFont) { this.globalFont = globalFont; } /** * @return global font size */ public CSSLength getGlobalFontSize() { return globalFontSize; } public void setGlobalFontSize(CSSLength globalFontSize) { this.globalFontSize = globalFontSize; globalFontSizeInHalfPoints = globalFontSize == null ? 0 : globalFontSize.toHalfPoints(); } public int getGlobalFontSizeInHalfPoints() { return globalFontSizeInHalfPoints; } /** * @return whether to show default table borders if the table inside a table cell */ public boolean isShowDefaultTableBorderInTableCell() { return showDefaultTableBorderInTableCell; } public void setShowDefaultTableBorderInTableCell(boolean showDefaultTableBorderInTableCell) { this.showDefaultTableBorderInTableCell = showDefaultTableBorderInTableCell; } /** * @return custom html tag renderers */ public List getCustomRenderers() { return customRenderers; } public void setCustomRenderers(List customRenderers) { this.customRenderers = customRenderers; } /** * @return custom numbering indent */ public int getNumberingIndent() { return numberingIndent; } public void setNumberingIndent(int numberingIndent) { this.numberingIndent = numberingIndent; } /** * @return custom numbering hanging */ public int getNumberingHanging() { return numberingHanging; } public void setNumberingHanging(int numberingHanging) { this.numberingHanging = numberingHanging; } /** * @return custom numbering spacing */ public STLevelSuffix.Enum getNumberingSpacing() { return numberingSpacing; } public void setNumberingSpacing(STLevelSuffix.Enum numberingSpacing) { this.numberingSpacing = numberingSpacing; } public MathRenderConfig getMathRenderConfig() { return mathRenderConfig; } } ================================================ FILE: src/main/java/org/ddr/poi/html/HtmlRenderContext.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.html; import com.deepoove.poi.render.RenderContext; import com.steadystate.css.dom.CSSStyleDeclarationImpl; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.math.NumberUtils; import org.apache.poi.openxml4j.exceptions.InvalidFormatException; import org.apache.poi.util.Units; import org.apache.poi.xwpf.usermodel.BodyType; import org.apache.poi.xwpf.usermodel.IBody; import org.apache.poi.xwpf.usermodel.IRunBody; import org.apache.poi.xwpf.usermodel.SVGPictureData; import org.apache.poi.xwpf.usermodel.SVGRelation; import org.apache.poi.xwpf.usermodel.XWPFDocument; import org.apache.poi.xwpf.usermodel.XWPFFooter; import org.apache.poi.xwpf.usermodel.XWPFFootnote; import org.apache.poi.xwpf.usermodel.XWPFHeader; import org.apache.poi.xwpf.usermodel.XWPFHyperlinkRun; import org.apache.poi.xwpf.usermodel.XWPFParagraph; import org.apache.poi.xwpf.usermodel.XWPFPicture; import org.apache.poi.xwpf.usermodel.XWPFRelation; import org.apache.poi.xwpf.usermodel.XWPFRun; import org.apache.poi.xwpf.usermodel.XWPFStyle; import org.apache.poi.xwpf.usermodel.XWPFStyles; import org.apache.poi.xwpf.usermodel.XWPFTable; import org.apache.poi.xwpf.usermodel.XWPFTableCell; import org.apache.xmlbeans.XmlCursor; import org.apache.xmlbeans.XmlObject; import org.apache.xmlbeans.impl.xb.xmlschema.SpaceAttribute; import org.ddr.poi.html.util.CSSLength; import org.ddr.poi.html.util.CSSLengthUnit; import org.ddr.poi.html.util.CSSStyleUtils; import org.ddr.poi.html.util.Colors; import org.ddr.poi.html.util.InlineStyle; import org.ddr.poi.html.util.NamedFontSize; import org.ddr.poi.html.util.NumberingContext; import org.ddr.poi.html.util.RenderUtils; import org.ddr.poi.html.util.WhiteSpaceRule; import org.ddr.poi.html.util.XWPFParagraphRuns; import org.ddr.poi.math.MathMLUtils; import org.ddr.poi.math.MathRenderConfig; import org.ddr.poi.util.XmlUtils; import org.jsoup.internal.StringUtil; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.nodes.Node; import org.jsoup.nodes.TextNode; import org.openxmlformats.schemas.drawingml.x2006.main.CTBlip; import org.openxmlformats.schemas.drawingml.x2006.main.CTGraphicalObjectFrameLocking; import org.openxmlformats.schemas.drawingml.x2006.main.CTNonVisualGraphicFrameProperties; import org.openxmlformats.schemas.drawingml.x2006.main.CTOfficeArtExtension; import org.openxmlformats.schemas.drawingml.x2006.main.CTOfficeArtExtensionList; import org.openxmlformats.schemas.drawingml.x2006.picture.CTPicture; import org.openxmlformats.schemas.drawingml.x2006.wordprocessingDrawing.CTAnchor; import org.openxmlformats.schemas.drawingml.x2006.wordprocessingDrawing.CTInline; import org.openxmlformats.schemas.drawingml.x2006.wordprocessingDrawing.CTPosH; import org.openxmlformats.schemas.drawingml.x2006.wordprocessingDrawing.CTPosV; import org.openxmlformats.schemas.drawingml.x2006.wordprocessingDrawing.STAlignH; import org.openxmlformats.schemas.drawingml.x2006.wordprocessingDrawing.STAlignV; import org.openxmlformats.schemas.drawingml.x2006.wordprocessingDrawing.STRelFromH; import org.openxmlformats.schemas.drawingml.x2006.wordprocessingDrawing.STRelFromV; import org.openxmlformats.schemas.drawingml.x2006.wordprocessingDrawing.STWrapText; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTColor; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTDrawing; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTFonts; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTHyperlink; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTMarkupRange; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTP; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPageMar; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPageSz; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTRPr; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSectPr; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTShd; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTStyle; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTbl; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblBorders; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTText; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTUnderline; import org.openxmlformats.schemas.wordprocessingml.x2006.main.STBorder; import org.openxmlformats.schemas.wordprocessingml.x2006.main.STOnOff; import org.openxmlformats.schemas.wordprocessingml.x2006.main.STShd; import org.openxmlformats.schemas.wordprocessingml.x2006.main.STStyleType; import org.openxmlformats.schemas.wordprocessingml.x2006.main.STThemeColor; import org.openxmlformats.schemas.wordprocessingml.x2006.main.STUnderline; import org.openxmlformats.schemas.wordprocessingml.x2006.main.STVerticalAlignRun; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.xml.namespace.QName; import java.io.IOException; import java.io.InputStream; import java.math.BigInteger; import java.net.URI; import java.util.ArrayList; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; /** * HTML字符串渲染上下文 * * @author Draco * @since 2021-02-08 */ public class HtmlRenderContext extends RenderContext { private static final Logger log = LoggerFactory.getLogger(HtmlRenderContext.class); /** * 默认字号 小四 12pt 16px */ private static final CSSLength DEFAULT_FONT_SIZE = new CSSLength(12, CSSLengthUnit.PT); /** * 默认超链接颜色 */ private static final String DEFAULT_HYPERLINK_COLOR = "0563C1"; /** * HTML元素渲染器提供者 */ private final ElementRendererProvider rendererProvider; /** * 父容器(子元素通常为段落/表格)栈,主要用于渲染HTML表格时父容器的切换 */ private final LinkedList ancestors = new LinkedList<>(); /** * 行内样式栈,最近声明的样式最先生效 */ private final LinkedList inlineStyles = new LinkedList<>(); /** * 字号栈,一些相对大小的字号值将被进行换算 */ private final LinkedList fontSizesInHalfPoints = new LinkedList<>(); /** * 列表上下文,用于处理嵌套列表 */ private final NumberingContext numberingContext; /** * 默认字号 */ private final CSSLength defaultFontSize; /** * 页面宽度 */ private final CSSLength pageWidth; /** * 页面高度 */ private final CSSLength pageHeight; /** * 页面顶部边距 */ private final CSSLength marginTop; /** * 页面右侧边距 */ private final CSSLength marginRight; /** * 页面底部边距 */ private final CSSLength marginBottom; /** * 页面左侧边距 */ private final CSSLength marginLeft; /** * 可用页面宽度 */ private final int availablePageWidth; /** * 可用页面高度 */ private final int availablePageHeight; /** * 占位符所在段落的样式ID */ private String placeholderStyleId; /** * 当前Run元素,可能为超链接 */ private XWPFRun currentRun; /** * 全局字体,声明后所有样式中的字体将失效 */ private String globalFont; /** * 全局字号,声明后所有样式中的字号将失效 */ private BigInteger globalFontSize; /** * 嵌套表格是否默认显示边框 */ private boolean showDefaultTableBorderInTableCell; /** * 数学公式渲染配置 */ private MathRenderConfig mathRenderConfig; /** * 块状元素深度计数器 */ private int blockLevel; /** * 全局xml指针,保证其总是处于将要插入内容的位置,仅在需要移动之前进行push,适时pop还原位置 */ private final XmlCursor globalCursor; /** * 防重段落 */ private XWPFParagraph dedupeParagraph; /** * 同一段落内的前一个文本节点 */ private TextWrapper previousText; /** * 前一个图片所在节点 */ private CTR previousDrawingRun; /** * 构造方法 * * @param context 原始渲染上下文 */ public HtmlRenderContext(RenderContext context, ElementRendererProvider rendererProvider) { super(context.getEleTemplate(), context.getData(), context.getTemplate()); this.rendererProvider = rendererProvider; globalCursor = getRun().getCTR().newCursor(); numberingContext = new NumberingContext(getXWPFDocument()); int w = RenderUtils.A4_WIDTH; int h = RenderUtils.A4_HEIGHT; int top = RenderUtils.DEFAULT_TOP_MARGIN; int right = RenderUtils.DEFAULT_RIGHT_MARGIN; int bottom = RenderUtils.DEFAULT_BOTTOM_MARGIN; int left = RenderUtils.DEFAULT_LEFT_MARGIN; CTSectPr sectPr = getXWPFDocument().getDocument().getBody().getSectPr(); if (sectPr != null) { CTPageSz pgSz = sectPr.getPgSz(); // 页面尺寸单位是twip if (pgSz != null) { w = pgSz.getW().intValue(); h = pgSz.getH().intValue(); } CTPageMar pgMar = sectPr.getPgMar(); if (pgMar != null) { top = pgMar.getTop().intValue(); right = pgMar.getRight().intValue(); bottom = pgMar.getBottom().intValue(); left = pgMar.getLeft().intValue(); } } pageWidth = new CSSLength(w, CSSLengthUnit.TWIP); pageHeight = new CSSLength(h, CSSLengthUnit.TWIP); marginTop = new CSSLength(top, CSSLengthUnit.TWIP); marginRight = new CSSLength(right, CSSLengthUnit.TWIP); marginBottom = new CSSLength(bottom, CSSLengthUnit.TWIP); marginLeft = new CSSLength(left, CSSLengthUnit.TWIP); availablePageWidth = new CSSLength(w - left - right, CSSLengthUnit.TWIP).toEMU(); availablePageHeight = new CSSLength(h - top - bottom, CSSLengthUnit.TWIP).toEMU(); int fontSize = getXWPFDocument().getStyles().getDefaultRunStyle().getFontSize(); defaultFontSize = fontSize > 0 ? new CSSLength(fontSize, CSSLengthUnit.PT) : DEFAULT_FONT_SIZE; extractPlaceholderStyle(); } /** * 抽取占位符所在段落的样式 */ private void extractPlaceholderStyle() { XWPFRun run = getRun(); IRunBody runParent = run.getParent(); if (runParent instanceof XWPFParagraph) { XWPFParagraph paragraph = (XWPFParagraph) runParent; String styleId = paragraph.getStyleID(); boolean existsRPr = run.getCTR().isSetRPr(); if (styleId == null && !existsRPr) { return; } else if (styleId != null && !existsRPr) { placeholderStyleId = styleId; return; } XWPFStyles styles = getXWPFDocument().getStyles(); CTStyle newCTStyle = CTStyle.Factory.newInstance(); newCTStyle.setCustomStyle(STOnOff.TRUE); newCTStyle.setType(STStyleType.PARAGRAPH); newCTStyle.addNewHidden(); newCTStyle.setRPr(run.getCTR().getRPr()); XmlUtils.removeNamespaces(newCTStyle.getRPr()); String newStyleId = styleId + styles.getNumberOfStyles(); newCTStyle.setStyleId(newStyleId); newCTStyle.addNewName().setVal(newStyleId); placeholderStyleId = newStyleId; if (styleId != null) { newCTStyle.addNewBasedOn().setVal(styleId); } XWPFStyle newStyle = new XWPFStyle(newCTStyle, styles); styles.addStyle(newStyle); paragraph.setStyle(newStyleId); } } @Override public IBody getContainer() { IBody container = ancestors.peek(); return container == null ? super.getContainer() : container; } /** * 父容器入栈 * * @param body 父容器 */ public void pushContainer(IBody body) { ancestors.push(body); } /** * 父容器出栈 */ public void popContainer() { ancestors.pop(); } /** * 获取最近的段落,如果当前最近位置的内容元素是表格,则创建一个与之平级的段落 * * @return 最近的段落 */ public XWPFParagraph getClosestParagraph() { if (globalCursor.getObject() == getRun().getCTR()) { return (XWPFParagraph) getRun().getParent(); } globalCursor.push(); XWPFParagraph paragraph = null; if (globalCursor.toPrevSibling()) { XmlObject object = globalCursor.getObject(); if (object instanceof CTP) { paragraph = getContainer().getParagraph((CTP) object); } else { // pop() is safer than toNextSibling() globalCursor.pop(); globalCursor.push(); paragraph = newParagraph(null, globalCursor); RenderUtils.paragraphStyle(this, paragraph, CSSStyleUtils.EMPTY_STYLE); } } globalCursor.pop(); if (paragraph != null) { return paragraph; } throw new IllegalStateException("No paragraph in stack"); } /** * 开始渲染超链接 * * @param uri 链接地址 */ public void startHyperlink(String uri) { try { URI.create(uri); } catch (Exception e) { log.warn("Illegal href", e); uri = "#"; } if (isBlocked()) { XWPFParagraph paragraph = getClosestParagraph(); currentRun = paragraph.createHyperlinkRun(uri); if (dedupeParagraph == paragraph) { unmarkDedupe(); } } else { // 在占位符之前插入超链接 String rId = getRun().getParent().getPart().getPackagePart() .addExternalRelationship(uri, XWPFRelation.HYPERLINK.getRelation()).getId(); XmlCursor xmlCursor = getRun().getCTR().newCursor(); xmlCursor.insertElement(XmlUtils.HYPERLINK_QNAME); xmlCursor.toPrevSibling(); CTHyperlink ctHyperlink = (CTHyperlink) xmlCursor.getObject(); xmlCursor.dispose(); ctHyperlink.setId(rId); ctHyperlink.addNewR(); currentRun = new XWPFHyperlinkRun(ctHyperlink, ctHyperlink.getRArray(0), getRun().getParent()); } } /** * 结束渲染超链接 */ public void endHyperlink() { currentRun = null; } /** * 新建段落 * * @param container 容器 * @param cursor xml指针 * @return 段落 */ public XWPFParagraph newParagraph(IBody container, XmlCursor cursor) { if (container == null) { container = getContainer(); } XWPFParagraph xwpfParagraph = container.insertNewParagraph(cursor); if (placeholderStyleId != null) { xwpfParagraph.setStyle(placeholderStyleId); } markDedupe(xwpfParagraph); previousText = null; adjustPicture(); return xwpfParagraph; } private void adjustPicture() { if (previousDrawingRun != null) { XmlCursor xmlCursor = previousDrawingRun.newCursor(); List drawings = new ArrayList<>(previousDrawingRun.getDrawingList()); boolean hasText = false; while (xmlCursor.toPrevSibling()) { if (XmlUtils.R_QNAME.equals(xmlCursor.getName())) { CTR ctr = (CTR) xmlCursor.getObject(); for (int i = 0, l = ctr.sizeOfTArray(); i < l; i++) { CTText ctText = ctr.getTArray(i); if (StringUtils.isNotBlank(ctText.getStringValue())) { hasText = true; break; } } for (int i = 0, l = ctr.sizeOfDrawingArray(); i < l; i++) { drawings.add(ctr.getDrawingArray(i)); } } else if (MathMLUtils.OMATH_QNAME.equals(xmlCursor.getName())) { hasText = true; } } if (!hasText) { for (CTDrawing drawing : drawings) { CTAnchor ctAnchor = RenderUtils.inlineToAnchor(drawing); ctAnchor.addNewWrapTopAndBottom(); CTPosH ctPosH = ctAnchor.addNewPositionH(); ctPosH.setRelativeFrom(STRelFromH.MARGIN); ctPosH.setAlign(STAlignH.LEFT); CTPosV ctPosV = ctAnchor.addNewPositionV(); ctPosV.setRelativeFrom(STRelFromV.PARAGRAPH); ctPosV.setAlign(STAlignV.TOP); } } xmlCursor.dispose(); previousDrawingRun = null; } } /** * 新建CTR * * @return CTR */ public CTR newRun() { // 超链接虽然不是段落,但是内部可以容纳多个run if (currentRun instanceof XWPFHyperlinkRun) { XmlCursor xmlCursor = currentRun.getCTR().newCursor(); CTR ctr; if (xmlCursor.toFirstChild()) { ctr = ((XWPFHyperlinkRun) currentRun).getCTHyperlink().addNewR(); } else { // run没有内容则直接复用 ctr = currentRun.getCTR(); } xmlCursor.dispose(); // 默认链接样式 initHyperlinkStyle(ctr); return ctr; } // 考虑到样式可能不一致,总是创建新的run if (isBlocked()) { XWPFParagraph paragraph = getClosestParagraph(); currentRun = paragraph.createRun(); if (dedupeParagraph == paragraph) { unmarkDedupe(); } } else { // 在占位符之前插入run XmlCursor xmlCursor = getRun().getCTR().newCursor(); xmlCursor.insertElement(XmlUtils.R_QNAME); xmlCursor.toPrevSibling(); CTR ctr = (CTR) xmlCursor.getObject(); xmlCursor.dispose(); currentRun = new XWPFRun(ctr, getRun().getParent()); } return currentRun.getCTR(); } /** * 初始化超链接样式 * * @param ctr CTR */ private void initHyperlinkStyle(CTR ctr) { CTRPr rPr = RenderUtils.getRPr(ctr); CTColor ctColor = rPr.addNewColor(); ctColor.setVal(DEFAULT_HYPERLINK_COLOR); ctColor.setThemeColor(STThemeColor.HYPERLINK); rPr.addNewU().setVal(STUnderline.SINGLE); } /** * 获取最近的表格,仅可在渲染表格及其内部元素的时候使用 * * @return 最近的表格 */ public XWPFTable getClosestTable() { globalCursor.push(); XWPFTable table = null; if (globalCursor.toPrevSibling()) { XmlObject object = globalCursor.getObject(); if (object instanceof CTTbl) { table = getContainer().getTable((CTTbl) object); } } globalCursor.pop(); if (table != null) { return table; } throw new IllegalStateException("No table in stack"); } /** * 行内样式入栈 * * @param inlineStyle 样式声明 * @param block 是否为块状元素 */ public void pushInlineStyle(CSSStyleDeclarationImpl inlineStyle, boolean block) { String newFontSize = inlineStyle.getFontSize(); // 默认值表示未声明字号 int fontSize = Integer.MIN_VALUE; if (StringUtils.isNotBlank(newFontSize)) { NamedFontSize namedFontSize = NamedFontSize.of(newFontSize); if (namedFontSize != null) { // 固定名称的字号 fontSize = namedFontSize.getSize().toHalfPoints(); } else if (HtmlConstants.SMALLER.equalsIgnoreCase(newFontSize)) { // 相对小一号 int inheritedFontSize = getInheritedFontSizeInHalfPoints(); fontSize = RenderUtils.smallerFontSizeInHalfPoints(inheritedFontSize); } else if (HtmlConstants.LARGER.equalsIgnoreCase(newFontSize)) { // 相对大一号 int inheritedFontSize = getInheritedFontSizeInHalfPoints(); fontSize = RenderUtils.largerFontSizeInHalfPoints(inheritedFontSize); } else { CSSLength cssLength = CSSLength.of(newFontSize); if (cssLength.isValid()) { if (cssLength.getUnit() == CSSLengthUnit.PERCENT) { fontSize = (int) Math.rint(getInheritedFontSizeInHalfPoints() * cssLength.getValue() * cssLength.getUnit().absoluteFactor()); } else { int emu = lengthToEMU(cssLength); fontSize = emu * 2 / Units.EMU_PER_POINT; } } } } fontSizesInHalfPoints.push(fontSize); // text-decoration-line 在继承时需要合并 String textDecorationLine = inlineStyle.getPropertyValue(HtmlConstants.CSS_TEXT_DECORATION_LINE); if (StringUtils.isNotBlank(textDecorationLine) && !HtmlConstants.NONE.equals(textDecorationLine)) { Set remainValues = new HashSet<>(HtmlConstants.TEXT_DECORATION_LINES); String[] values = StringUtils.split(textDecorationLine, ' '); for (String value : values) { remainValues.remove(value); } if (!remainValues.isEmpty()) { StringBuilder lines = new StringBuilder(textDecorationLine); for (InlineStyle inheritedStyle : inlineStyles) { String s = inheritedStyle.getDeclaration().getPropertyValue(HtmlConstants.CSS_TEXT_DECORATION_LINE); if (HtmlConstants.NONE.equals(s)) { break; } else if (remainValues.contains(s)) { lines.append(' ').append(s); remainValues.remove(s); if (remainValues.isEmpty()) { break; } } } if (lines.length() > textDecorationLine.length()) { inlineStyle.setProperty(HtmlConstants.CSS_TEXT_DECORATION_LINE, lines.toString(), null); } } } inlineStyles.push(new InlineStyle(inlineStyle, block)); } /** * 行内样式出栈 */ public void popInlineStyle() { fontSizesInHalfPoints.pop(); inlineStyles.pop(); } /** * 当前元素的样式声明,在HTML元素渲染开始时立即调用才可得到正确的声明,因为在解析的过程中可能会动态插入样式 * * @return 当前元素的样式声明 */ public CSSStyleDeclarationImpl currentElementStyle() { InlineStyle inlineStyle = inlineStyles.peek(); return inlineStyle == null ? CSSStyleUtils.EMPTY_STYLE : inlineStyle.getDeclaration(); } /** * 获取样式值,将被转换为小写 * * @param property 样式名称 * @return 样式值,未声明时返回空字符串 */ public String getPropertyValue(String property) { return getPropertyValue(property, false); } /** * 获取样式值,将被转换为小写 * * @param property 样式名称 * @param inlineOnly 是否仅获取行内元素样式 * @return 样式值,未声明时返回空字符串 */ public String getPropertyValue(String property, boolean inlineOnly) { return getPropertyValue(property, false, inlineOnly); } /** * 获取样式值 * * @param property 样式名称 * @param caseSensitive 是否大小写无关,如果无关则将转换为小写,否则保留原始值 * @param inlineOnly 是否仅获取行内元素样式 * @return 样式值,未声明时返回空字符串 */ public String getPropertyValue(String property, boolean caseSensitive, boolean inlineOnly) { for (InlineStyle inlineStyle : inlineStyles) { if (inlineOnly && inlineStyle.isBlock()) { break; } String propertyValue = inlineStyle.getDeclaration().getPropertyValue(property); if (StringUtils.isNotBlank(propertyValue)) { return caseSensitive ? propertyValue : propertyValue.toLowerCase(); } } return ""; } /** * @return Word中设置的默认字号 */ public CSSLength getDefaultFontSize() { return defaultFontSize; } /** * @return 获取当前元素继承的字号,以“半点”为单位 */ public int getInheritedFontSizeInHalfPoints() { for (Integer fontSize : fontSizesInHalfPoints) { if (fontSize > 0) { return fontSize; } } return defaultFontSize.toHalfPoints(); } /** * @return 父容器的可用宽度,以EMU为单位 */ public int getAvailableWidthInEMU() { IBody container = getContainer(); if (container.getPartType() == BodyType.DOCUMENT) { return availablePageWidth; } else { return RenderUtils.getAvailableWidthInEMU(container); } } /** * 考虑约束计算长度,以EMU为单位 * * @param length 长度声明 * @param maxLength 最大长度声明 * @param naturalEMU 原始长度 * @param parentEMU 父容器长度 * @return 以EMU为单位的长度值 */ public int computeLengthInEMU(String length, String maxLength, int naturalEMU, int parentEMU) { int emu = naturalEMU; if (length.length() > 0) { CSSLength cssLength = CSSLength.of(length); if (cssLength.isValid()) { emu = computeLengthInEMU(cssLength, naturalEMU, parentEMU); } } if (maxLength.length() > 0) { CSSLength cssLength = CSSLength.of(maxLength); if (cssLength.isValid()) { int maxEMU = computeLengthInEMU(cssLength, naturalEMU, parentEMU); emu = Math.min(maxEMU, emu); } } return Math.min(emu, parentEMU); } /** * 考虑约束计算长度,以EMU为单位 * * @param cssLength 长度声明 * @param naturalEMU 原始长度 * @param parentEMU 父容器长度 * @return 以EMU为单位的长度值 */ public int computeLengthInEMU(CSSLength cssLength, int naturalEMU, int parentEMU) { int length; if (cssLength.getUnit() == CSSLengthUnit.PERCENT) { if (parentEMU != Integer.MAX_VALUE) { length = (int) (parentEMU * cssLength.getValue() * cssLength.getUnit().absoluteFactor()); } else { length = naturalEMU; } } else { length = lengthToEMU(cssLength); } return length; } /** * 渲染文本 * * @param text 文本 */ public void renderText(String text) { String whiteSpace = getPropertyValue(HtmlConstants.CSS_WHITE_SPACE); WhiteSpaceRule rule = WhiteSpaceRule.of(whiteSpace, WhiteSpaceRule.NORMAL); StringBuilder sb = StringUtil.borrowBuilder(); boolean mergeWhitespace = false; // https://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references int len = text.length(); int c; if (!rule.isKeepTrailingSpace()) { boolean reachedLastNonWhite = false; for (int i = len - 1; i >= 0; i -= Character.charCount(c)) { c = text.codePointAt(i); switch (c) { case ' ': case '\r': case '\n': case '\t': case 173: // soft hyphen case 8203: // zero width space case 8204: // zero width non-joiner case 8205: // zero width joiner case 8206: // lrm case 8207: // rlm case 8288: // word joiner case 8289: // apply function case 8290: // invisible times case 8291: // invisible separator len = i; break; default: if (Character.getType(c) == 16) { len = i; } else { reachedLastNonWhite = true; } break; } if (reachedLastNonWhite) { break; } } } if (len == 0) { return; } boolean endTrimmed = len < text.length(); CTR ctr = newRun(); for (int i = 0; i < len; i += Character.charCount(c)) { c = text.codePointAt(i); switch (c) { case '\r': if (i + 1 < len && text.codePointAt(i + 1) == '\n') { continue; } if (rule.isKeepLineBreak()) { addText(ctr, sb, false); ctr.addNewCr(); } else { mergeWhitespace = true; } break; case '\n': if (rule.isKeepLineBreak()) { addText(ctr, sb, false); ctr.addNewBr(); } else { mergeWhitespace = true; } break; case ' ': if (rule.isKeepSpaceAndTab()) { sb.appendCodePoint(c); } else { mergeWhitespace = true; } break; case '\t': if (rule.isKeepSpaceAndTab()) { addText(ctr, sb, false); ctr.addNewTab(); } else { mergeWhitespace = true; } break; case 160: // nbsp case 8192: // enquad case 8193: // emquad case 8194: // ensp case 8195: // emsp case 8196: // emsp13 case 8197: // emsp14 case 8199: // numsp case 8200: // puncsp case 8201: // thinsp case 8202: // hairsp case 8239: // narrow space case 8287: // medium space if (mergeWhitespace) { sb.append(' '); mergeWhitespace = false; } sb.append(' '); break; case 173: // soft hyphen case 8203: // zero width space case 8204: // zero width non-joiner case 8205: // zero width joiner case 8206: // lrm case 8207: // rlm case 8288: // word joiner case 8289: // apply function case 8290: // invisible times case 8291: // invisible separator continue; default: if (Character.getType(c) == 16) { continue; } if (mergeWhitespace) { if (sb.length() > 0) { sb.append(' '); } else if (previousText != null && !previousText.isEndTrimmed()) { sb.append(' '); previousText = null; } mergeWhitespace = false; } sb.appendCodePoint(c); if (previousText != null && previousText.isEndTrimmed()) { CTText previous = previousText.getText(); previous.setStringValue(previous.getStringValue() + ' '); previous.setSpace(SpaceAttribute.Space.PRESERVE); previousText = null; } break; } } addText(ctr, sb, endTrimmed); StringUtil.releaseBuilder(sb); // 应用样式 applyTextStyle(ctr); if (!(currentRun instanceof XWPFHyperlinkRun)) { currentRun = null; } } private void addText(CTR ctr, StringBuilder sb, boolean endTrimmed) { if (sb.length() > 0) { CTText ctText = ctr.addNewT(); String text = sb.toString(); ctText.setStringValue(text); if (text.charAt(0) == ' ' || text.charAt(sb.length() - 1) == ' ') { ctText.setSpace(SpaceAttribute.Space.PRESERVE); } sb.delete(0, sb.length()); previousText = new TextWrapper(ctText, endTrimmed); } } /** * 应用文本样式 * * @param ctr CTR */ private void applyTextStyle(CTR ctr) { CTRPr rPr = RenderUtils.getRPr(ctr); // 字体,如果声明了全局字体则忽略样式声明 String fontFamily = StringUtils.isBlank(globalFont) ? getPropertyValue(HtmlConstants.CSS_FONT_FAMILY) : globalFont; if (StringUtils.isNotBlank(fontFamily)) { CTFonts ctFonts = rPr.addNewRFonts(); // ASCII ctFonts.setAscii(fontFamily); // High ANSI ctFonts.setHAnsi(fontFamily); // Complex Script ctFonts.setCs(fontFamily); // East Asian ctFonts.setEastAsia(fontFamily); } // 字号 if (globalFontSize == null) { String fontSize = getPropertyValue(HtmlConstants.CSS_FONT_SIZE); if (StringUtils.isNotBlank(fontSize)) { int sz = getInheritedFontSizeInHalfPoints(); rPr.addNewSz().setVal(BigInteger.valueOf(sz)); } } else { // 如果定义了全局字号则忽略样式声明 rPr.addNewSz().setVal(globalFontSize); } // 加粗 String fontWeight = getPropertyValue(HtmlConstants.CSS_FONT_WEIGHT); if (fontWeight.contains(HtmlConstants.BOLD)) { rPr.addNewB(); } else if (NumberUtils.isParsable(fontWeight) && Float.parseFloat(fontWeight) > 500) { rPr.addNewB(); } // 斜体 String fontStyle = getPropertyValue(HtmlConstants.CSS_FONT_STYLE); if (HtmlConstants.ITALIC.equals(fontStyle) || HtmlConstants.OBLIQUE.equals(fontStyle)) { rPr.addNewI(); } // 颜色 String color = getPropertyValue(HtmlConstants.CSS_COLOR); if (StringUtils.isNotBlank(color)) { String hex = Colors.fromStyle(color); RenderUtils.getColor(rPr).setVal(hex); } String caps = getPropertyValue(HtmlConstants.CSS_FONT_VARIANT_CAPS); if (HtmlConstants.SMALL_CAPS.equals(caps)) { rPr.addNewSmallCaps(); } // 中划线/下划线 String textDecoration = getPropertyValue(HtmlConstants.CSS_TEXT_DECORATION_LINE); if (HtmlConstants.NONE.equals(textDecoration)) { RenderUtils.getUnderline(rPr).setVal(STUnderline.NONE); } else { if (StringUtils.contains(textDecoration, HtmlConstants.LINE_THROUGH)) { rPr.addNewStrike(); } if (StringUtils.contains(textDecoration, HtmlConstants.UNDERLINE)) { CTUnderline ctUnderline = RenderUtils.getUnderline(rPr); String textDecorationStyle = getPropertyValue(HtmlConstants.CSS_TEXT_DECORATION_STYLE); ctUnderline.setVal(RenderUtils.underline(textDecorationStyle)); String textDecorationColor = getPropertyValue(HtmlConstants.CSS_TEXT_DECORATION_COLOR); if (StringUtils.isNotBlank(textDecorationColor)) { String hex = Colors.fromStyle(textDecorationColor); ctUnderline.setColor(hex); } } } // 上下标 String verticalAlign = getPropertyValue(HtmlConstants.CSS_VERTICAL_ALIGN); if (HtmlConstants.SUPER.equals(verticalAlign)) { rPr.addNewVertAlign().setVal(STVerticalAlignRun.SUPERSCRIPT); } else if (HtmlConstants.SUB.equals(verticalAlign)) { rPr.addNewVertAlign().setVal(STVerticalAlignRun.SUBSCRIPT); } // FIXME 段落边框与行内边框分离,行内只有全边框,段落分四边 // 背景色 String backgroundColor = getPropertyValue(HtmlConstants.CSS_BACKGROUND_COLOR, true); if (StringUtils.isNotBlank(backgroundColor)) { String hex = Colors.fromStyle(backgroundColor, null); if (hex != null) { CTShd ctShd = rPr.addNewShd(); ctShd.setFill(hex); ctShd.setVal(STShd.CLEAR); } } // 可见性 String visibility = getPropertyValue(HtmlConstants.CSS_VISIBILITY); if (HtmlConstants.HIDDEN.equals(visibility) || HtmlConstants.COLLAPSE.equals(visibility)) { rPr.addNewVanish(); } } /** * 渲染图片 * * @param pictureData 图片数据流 * @param pictureType 图片类型 * @param filename 文件名 * @param width 宽度 * @param height 高度 * @param svgData SVG数据 */ public void renderPicture(InputStream pictureData, int pictureType, String filename, int width, int height, byte[] svgData) throws IOException, InvalidFormatException { CTR ctr = newRun(); XWPFPicture xwpfPicture = currentRun.addPicture(pictureData, pictureType, filename, width, height); CTR r = currentRun.getCTR(); boolean isSvg = svgData != null; if (isSvg) { attachSvgData(xwpfPicture, svgData); } CSSStyleDeclarationImpl styleDeclaration = currentElementStyle(); String cssFloat = styleDeclaration.getPropertyValue(HtmlConstants.CSS_FLOAT); boolean floatLeft = HtmlConstants.LEFT.equals(cssFloat); boolean floatRight = !floatLeft && HtmlConstants.RIGHT.equals(cssFloat); boolean floatCenter = !floatLeft && !floatRight && HtmlConstants.AUTO.equals(styleDeclaration.getPropertyValue(HtmlConstants.CSS_MARGIN_LEFT)) && HtmlConstants.AUTO.equals(styleDeclaration.getPropertyValue(HtmlConstants.CSS_MARGIN_RIGHT)); // vertical-align seems not working boolean floated = floatLeft || floatRight || floatCenter; CTDrawing drawing = null; if (r != ctr) { int lastDrawingIndex = r.sizeOfDrawingArray() - 1; drawing = r.getDrawingArray(lastDrawingIndex); ctr.setDrawingArray(new CTDrawing[]{drawing}); r.removeDrawing(lastDrawingIndex); drawing = ctr.getDrawingArray(0); } else if (isSvg || floated) { drawing = ctr.getDrawingArray(ctr.sizeOfDrawingArray() - 1); } previousDrawingRun = ctr; if (drawing != null && drawing.sizeOfInlineArray() > 0) { if (floated) { previousDrawingRun = null; CTAnchor ctAnchor = RenderUtils.inlineToAnchor(drawing); CTPosH ctPosH = ctAnchor.addNewPositionH(); ctPosH.setRelativeFrom(STRelFromH.MARGIN); if (floatCenter) { moveContentToNewPrevParagraph(ctr); ctAnchor.addNewWrapTopAndBottom(); ctPosH.setAlign(STAlignH.CENTER); } else { ctAnchor.addNewWrapSquare().setWrapText(STWrapText.LARGEST); ctPosH.setAlign(floatRight ? STAlignH.RIGHT : STAlignH.LEFT); } CTPosV ctPosV = ctAnchor.addNewPositionV(); ctPosV.setRelativeFrom(STRelFromV.PARAGRAPH); ctPosV.setAlign(STAlignV.TOP); if (isSvg) { CTNonVisualGraphicFrameProperties properties = ctAnchor.addNewCNvGraphicFramePr(); CTGraphicalObjectFrameLocking frameLocking = properties.addNewGraphicFrameLocks(); frameLocking.setNoChangeAspect(true); } } else if (isSvg) { CTInline ctInline = drawing.getInlineArray(0); CTNonVisualGraphicFrameProperties properties = ctInline.isSetCNvGraphicFramePr() ? ctInline.getCNvGraphicFramePr() : ctInline.addNewCNvGraphicFramePr(); CTGraphicalObjectFrameLocking frameLocking = properties.isSetGraphicFrameLocks() ? properties.getGraphicFrameLocks() : properties.addNewGraphicFrameLocks(); frameLocking.setNoChangeAspect(true); } } } /** * 附加SVG数据 * * @param xwpfPicture 图片 * @param svgData SVG数据 * @throws InvalidFormatException 非法格式 */ private void attachSvgData(XWPFPicture xwpfPicture, byte[] svgData) throws InvalidFormatException { CTPicture ctPicture = xwpfPicture.getCTPicture(); String svgRelId = getXWPFDocument().addPictureData(svgData, SVGPictureData.PICTURE_TYPE_SVG); CTBlip blip = ctPicture.getBlipFill().getBlip(); if (blip != null) { CTOfficeArtExtensionList extList = blip.isSetExtLst() ? blip.getExtLst() : blip.addNewExtLst(); CTOfficeArtExtension svgBitmap = extList.addNewExt(); svgBitmap.setUri(SVGRelation.SVG_URI); XmlCursor cur = svgBitmap.newCursor(); cur.toEndToken(); cur.beginElement(SVGRelation.SVG_QNAME); cur.insertNamespace(SVGRelation.SVG_PREFIX, SVGRelation.MS_SVG_NS); cur.insertAttributeWithValue(SVGRelation.EMBED_TAG, svgRelId); cur.dispose(); } } /** * 将长度换算为EMU * * @param length 长度 * @return EMU */ public int lengthToEMU(CSSLength length) { if (!length.isValid()) { throw new UnsupportedOperationException("Invalid CSS length"); } if (!length.getUnit().isRelative()) { return length.toEMU(); } double emu; switch (length.getUnit()) { case REM: emu = length.unitValue() * getDefaultFontSize().toEMU(); break; case EM: emu = length.unitValue() * getInheritedFontSizeInHalfPoints() * Units.EMU_PER_POINT / 2; break; case VW: emu = length.unitValue() * getPageWidth().toEMU(); break; case VH: emu = length.unitValue() * getPageHeight().toEMU(); break; case VMIN: emu = length.unitValue() * Math.min(getPageWidth().toEMU(), getPageHeight().toEMU()); break; case VMAX: emu = length.unitValue() * Math.max(getPageWidth().toEMU(), getPageHeight().toEMU()); break; // Unable to determine the use of width or height as a relative length for percent unit default: throw new UnsupportedOperationException("Can not convert to EMU with length: " + length); } return (int) Math.rint(emu); } public NumberingContext getNumberingContext() { return numberingContext; } public CSSLength getPageWidth() { return pageWidth; } public CSSLength getPageHeight() { return pageHeight; } public CSSLength getMarginTop() { return marginTop; } public CSSLength getMarginRight() { return marginRight; } public CSSLength getMarginBottom() { return marginBottom; } public CSSLength getMarginLeft() { return marginLeft; } public int getAvailablePageWidth() { return availablePageWidth; } public int getAvailablePageHeight() { return availablePageHeight; } public MathRenderConfig getMathRenderConfig() { return mathRenderConfig; } public void setMathRenderConfig(MathRenderConfig mathRenderConfig) { this.mathRenderConfig = mathRenderConfig; } public XWPFRun getCurrentRun() { return currentRun; } public String getGlobalFont() { return globalFont; } public BigInteger getGlobalFontSize() { return globalFontSize; } public boolean isShowDefaultTableBorderInTableCell() { return showDefaultTableBorderInTableCell; } public void setShowDefaultTableBorderInTableCell(boolean showDefaultTableBorderInTableCell) { this.showDefaultTableBorderInTableCell = showDefaultTableBorderInTableCell; } public void setGlobalFont(String globalFont) { this.globalFont = globalFont; } public void setGlobalFontSize(BigInteger globalFontSize) { this.globalFontSize = globalFontSize; } public boolean isBlocked() { return blockLevel > 0; } public void incrementBlockLevel() { blockLevel++; } public void decrementBlockLevel() { blockLevel--; } public void renderDocument(Document document) { Element body = document.body(); Element html = body.parent(); if (html.hasAttr(HtmlConstants.ATTR_STYLE)) { pushInlineStyle(getCssStyleDeclaration(html), html.isBlock()); } if (body.hasAttr(HtmlConstants.ATTR_STYLE)) { pushInlineStyle(getCssStyleDeclaration(body), body.isBlock()); } for (Node node : body.childNodes()) { renderNode(node); } globalCursor.dispose(); } public void renderNode(Node node) { boolean isElement = node instanceof Element; if (isElement) { Element element = ((Element) node); renderElement(element); } else if (node instanceof TextNode) { renderText(((TextNode) node).getWholeText()); } } public void renderElement(Element element) { if (log.isDebugEnabled()) { log.info("Start rendering html tag: <{}{}>", element.normalName(), element.attributes()); } if (element.tag().isFormListed() || element.tag().isFormSubmittable()) { return; } CSSStyleDeclarationImpl cssStyleDeclaration = getCssStyleDeclaration(element); String display = cssStyleDeclaration.getPropertyValue(HtmlConstants.CSS_DISPLAY); if (HtmlConstants.NONE.equalsIgnoreCase(display)) { return; } pushInlineStyle(cssStyleDeclaration, element.isBlock()); ElementRenderer elementRenderer = rendererProvider.get(element.normalName()); boolean blocked = false; if (renderAsBlock(element, elementRenderer)) { if (element.childNodeSize() == 0 && !HtmlConstants.KEEP_EMPTY_TAGS.contains(element.normalName())) { popInlineStyle(); return; } if (!isBlocked()) { // 复制段落中占位符之前的部分内容 moveContentToNewPrevParagraph(getRun().getCTR()); } incrementBlockLevel(); blocked = true; IBody container = getContainer(); boolean isTableTag = HtmlConstants.TAG_TABLE.equals(element.normalName()); adjustCursor(container, isTableTag); if (isTableTag) { globalCursor.push(); XWPFTable xwpfTable = container.insertNewTbl(globalCursor); globalCursor.pop(); if (dedupeParagraph != null && !numberingContext.contains(dedupeParagraph)) { if (!dedupeParagraph.equals(getRun().getParent()) && isEmptyParagraph(dedupeParagraph)) { removeParagraph(container, dedupeParagraph); } unmarkDedupe(); } // 新增时会自动创建一行一列,会影响自定义的表格渲染逻辑,故删除 xwpfTable.removeRow(0); if (container.getPartType() == BodyType.TABLECELL && isShowDefaultTableBorderInTableCell()) { CTTbl ctTbl = xwpfTable.getCTTbl(); CTTblBorders tblBorders = RenderUtils.getTblBorders(ctTbl); tblBorders.addNewTop().setVal(STBorder.SINGLE); tblBorders.addNewLeft().setVal(STBorder.SINGLE); tblBorders.addNewBottom().setVal(STBorder.SINGLE); tblBorders.addNewRight().setVal(STBorder.SINGLE); tblBorders.addNewInsideH().setVal(STBorder.SINGLE); tblBorders.addNewInsideV().setVal(STBorder.SINGLE); } RenderUtils.tableStyle(this, xwpfTable, cssStyleDeclaration); } else if (shouldNewParagraph(element)) { globalCursor.push(); XWPFParagraph xwpfParagraph = newParagraph(container, globalCursor); globalCursor.pop(); if (xwpfParagraph == null) { log.warn("Can not add new paragraph for element: {}, attributes: {}", element.tagName(), element.attributes().html()); } RenderUtils.paragraphStyle(this, xwpfParagraph, cssStyleDeclaration); } else { RenderUtils.paragraphStyle(this, dedupeParagraph, cssStyleDeclaration); } } if (elementRenderer != null) { if (!elementRenderer.renderStart(element, this)) { renderElementEnd(element, this, elementRenderer, blocked); return; } } for (Node child : element.childNodes()) { renderNode(child); } renderElementEnd(element, this, elementRenderer, blocked); } private boolean isEmptyParagraph(XWPFParagraph paragraph) { for (XWPFRun run : paragraph.getRuns()) { if (StringUtils.isNotBlank(run.text())) { return false; } if (!run.getEmbeddedPictures().isEmpty()) { return false; } } CTP ctp = paragraph.getCTP(); return ctp.sizeOfOMathArray() == 0 && ctp.sizeOfOMathParaArray() == 0; } private void removeParagraph(IBody container, XWPFParagraph paragraph) { switch (container.getPartType()) { case CONTENTCONTROL: break; case DOCUMENT: XWPFDocument xwpfDocument = (XWPFDocument) container; int posOfParagraph = xwpfDocument.getPosOfParagraph(paragraph); xwpfDocument.removeBodyElement(posOfParagraph); break; case HEADER: XWPFHeader xwpfHeader = (XWPFHeader) container; xwpfHeader.removeParagraph(paragraph); break; case FOOTER: XWPFFooter xwpfFooter = (XWPFFooter) container; xwpfFooter.removeParagraph(paragraph); break; case FOOTNOTE: XWPFFootnote xwpfFootnote = (XWPFFootnote) container; xwpfFootnote.getParagraphs().remove(paragraph); break; case TABLECELL: XWPFTableCell xwpfTableCell = (XWPFTableCell) container; xwpfTableCell.removeParagraph(xwpfTableCell.getParagraphs().indexOf(paragraph)); break; } } /** * HTML元素是否按照块状进行渲染 * * @param element HTML元素 * @return 是否按照块状进行渲染 */ public boolean renderAsBlock(Element element) { return renderAsBlock(element, rendererProvider.get(element.normalName())); } /** * HTML元素是否按照块状进行渲染 * * @param element HTML元素 * @param elementRenderer 元素渲染器 * @return 是否按照块状进行渲染 */ private boolean renderAsBlock(Element element, ElementRenderer elementRenderer) { return element.isBlock() && (elementRenderer == null || elementRenderer.renderAsBlock()); } private boolean shouldNewParagraph(Element element) { if (dedupeParagraph == null) { return true; // return !HtmlConstants.TAG_HR.equals(element.normalName()); } boolean newParagraph = false; XmlCursor xmlCursor = dedupeParagraph.getCTP().newCursor(); xmlCursor.push(); if (xmlCursor.toPrevSibling()) { if (XmlUtils.P_QNAME.equals(xmlCursor.getName())) { newParagraph = removeLastBrRun(xmlCursor); } } if (!newParagraph) { xmlCursor.pop(); newParagraph = removeLastBrRun(xmlCursor); } xmlCursor.dispose(); return newParagraph; } private boolean removeLastBrRun(XmlCursor xmlCursor) { boolean removed = false; if (xmlCursor.toLastChild()) { if (XmlUtils.R_QNAME.equals(xmlCursor.getName())) { xmlCursor.push(); if (xmlCursor.toFirstChild() && XmlUtils.BR_QNAME.equals(xmlCursor.getName()) && !xmlCursor.toNextSibling()) { xmlCursor.pop(); xmlCursor.removeXml(); unmarkDedupe(); removed = true; } else { xmlCursor.pop(); } } } return removed; } private void adjustCursor(IBody container, boolean isTableTag) { if (XmlUtils.R_QNAME.equals(globalCursor.getName())) { globalCursor.push(); globalCursor.toParent(); } globalCursor.push(); // 如果是表格,检查当前word容器的前一个兄弟元素是否为表格,是则插入一个段落,防止表格粘连在一起 if (isTableTag && globalCursor.toPrevSibling()) { if (XmlUtils.TBL_QNAME.equals(globalCursor.getName())) { // pop() is safer than toNextSibling() globalCursor.pop(); globalCursor.push(); XWPFParagraph paragraph = newParagraph(container, globalCursor); unmarkDedupe(); RenderUtils.paragraphStyle(this, paragraph, CSSStyleUtils.EMPTY_STYLE); } } globalCursor.pop(); } private void renderElementEnd(Element element, HtmlRenderContext context, ElementRenderer elementRenderer, boolean blocked) { if (elementRenderer != null) { elementRenderer.renderEnd(element, context); } context.popInlineStyle(); if (blocked) { context.decrementBlockLevel(); } } private void moveContentToNewPrevParagraph(CTR ctr) { XmlCursor rCursor = ctr.newCursor(); boolean hasPrevSibling = false; while (rCursor.toPrevSibling()) { XmlObject object = rCursor.getObject(); if (object instanceof CTMarkupRange) { continue; } if (!XmlUtils.PPR_QNAME.equals(rCursor.getName())) { hasPrevSibling = true; break; } } if (!hasPrevSibling) { rCursor.dispose(); return; } rCursor.toParent(); rCursor.push(); CTP ctp = ((CTP) rCursor.getObject()); XWPFParagraph paragraph = getContainer().getParagraph(ctp); XWPFParagraph newParagraph = getContainer().insertNewParagraph(rCursor); XmlCursor pCursor = newParagraph.getCTP().newCursor(); pCursor.toEndToken(); rCursor.pop(); rCursor.toFirstChild(); XmlObject previousRun = null; if (previousText != null) { XmlCursor tCursor = previousText.getText().newCursor(); tCursor.toParent(); previousRun = tCursor.getObject(); tCursor.dispose(); } while (true) { XmlObject object = rCursor.getObject(); if (ctr == object) break; QName name = rCursor.getName(); if (XmlUtils.PPR_QNAME.equals(name)) { rCursor.copyXml(pCursor); rCursor.toNextSibling(); } else if (XmlUtils.BOOKMARK_START_QNAME.equals(name) || XmlUtils.BOOKMARK_END_QNAME.equals(name)) { rCursor.toNextSibling(); } else { if (previousDrawingRun == object) { previousDrawingRun = null; } if (previousRun == object) { previousText = null; } // moveXml附带了toNextSibling的效果 rCursor.moveXml(pCursor); } } rCursor.dispose(); pCursor.dispose(); XWPFParagraphRuns runs = new XWPFParagraphRuns(paragraph); for (int i = runs.runCount() - ctp.getRList().size() - 1; i >= 0; i--) { runs.remove(i); } } public CSSStyleDeclarationImpl getCssStyleDeclaration(Element element) { String style = element.attr(HtmlConstants.ATTR_STYLE); CSSStyleDeclarationImpl cssStyleDeclaration = CSSStyleUtils.parse(style); CSSStyleUtils.split(cssStyleDeclaration); return cssStyleDeclaration; } /** * 保存当前指针位置并移动到目标指针位置 * * @param targetCursor 目标指针 */ public void pushCursor(XmlCursor targetCursor) { globalCursor.push(); globalCursor.toCursor(targetCursor); } /** * 返回之前保存的指针位置 * * @return 是否返回成功 */ public boolean popCursor() { return globalCursor.pop(); } /** * @return 指针当前指向的对象 */ public XmlObject currentCursorObject() { return globalCursor.getObject(); } /** * 标记段落以防止块状元素嵌套产生多余的空段落 * * @param paragraph 段落 */ public void markDedupe(XWPFParagraph paragraph) { dedupeParagraph = paragraph; } /** * 取消段落防重标记 */ public void unmarkDedupe() { dedupeParagraph = null; } /** * 文本封装类,用于空白字符折叠处理 */ private static class TextWrapper { private final CTText text; private final boolean endTrimmed; public TextWrapper(CTText text, boolean endTrimmed) { this.text = text; this.endTrimmed = endTrimmed; } public CTText getText() { return text; } public boolean isEndTrimmed() { return endTrimmed; } } } ================================================ FILE: src/main/java/org/ddr/poi/html/HtmlRenderPolicy.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.html; import com.deepoove.poi.policy.AbstractRenderPolicy; import com.deepoove.poi.render.RenderContext; import org.apache.commons.lang3.StringUtils; import org.apache.poi.xwpf.usermodel.BodyElementType; import org.apache.poi.xwpf.usermodel.BodyType; import org.apache.poi.xwpf.usermodel.IBody; import org.apache.poi.xwpf.usermodel.IBodyElement; import org.apache.poi.xwpf.usermodel.XWPFRun; import org.apache.poi.xwpf.usermodel.XWPFTableCell; import org.apache.xmlbeans.XmlCursor; import org.ddr.poi.html.tag.ARenderer; import org.ddr.poi.html.tag.BigRenderer; import org.ddr.poi.html.tag.BoldRenderer; import org.ddr.poi.html.tag.BreakRenderer; import org.ddr.poi.html.tag.DeleteRenderer; import org.ddr.poi.html.tag.FigureCaptionRenderer; import org.ddr.poi.html.tag.FigureRenderer; import org.ddr.poi.html.tag.HeaderBreakRenderer; import org.ddr.poi.html.tag.HeaderRenderer; import org.ddr.poi.html.tag.ImageRenderer; import org.ddr.poi.html.tag.ItalicRenderer; import org.ddr.poi.html.tag.LaTeXRenderer; import org.ddr.poi.html.tag.ListItemRenderer; import org.ddr.poi.html.tag.ListRenderer; import org.ddr.poi.html.tag.MarkRenderer; import org.ddr.poi.html.tag.MathRenderer; import org.ddr.poi.html.tag.OmittedRenderer; import org.ddr.poi.html.tag.PreRenderer; import org.ddr.poi.html.tag.RubyRenderer; import org.ddr.poi.html.tag.SmallRenderer; import org.ddr.poi.html.tag.SubscriptRenderer; import org.ddr.poi.html.tag.SuperscriptRenderer; import org.ddr.poi.html.tag.SvgRenderer; import org.ddr.poi.html.tag.TableCellRenderer; import org.ddr.poi.html.tag.TableRenderer; import org.ddr.poi.html.tag.UnderlineRenderer; import org.ddr.poi.html.tag.WalkThroughRenderer; import org.ddr.poi.html.util.CSSLength; import org.ddr.poi.html.util.JsoupUtils; import org.ddr.poi.util.XmlUtils; import org.jsoup.nodes.Document; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR; import java.math.BigInteger; import java.util.HashMap; import java.util.List; import java.util.Map; /** * HTML字符串渲染策略 * * @author Draco * @since 2021-02-07 */ public class HtmlRenderPolicy extends AbstractRenderPolicy { private final Map elRenderers; private final HtmlRenderConfig config; public HtmlRenderPolicy() { this(new HtmlRenderConfig()); } @Deprecated public HtmlRenderPolicy(String globalFont, CSSLength globalFontSize) { this(new HtmlRenderConfig()); config.setGlobalFont(globalFont); config.setGlobalFontSize(globalFontSize); } public HtmlRenderPolicy(HtmlRenderConfig config) { ElementRenderer[] renderers = { new ARenderer(), new BigRenderer(), new BoldRenderer(), new BreakRenderer(), new DeleteRenderer(), new FigureRenderer(), new FigureCaptionRenderer(), new HeaderBreakRenderer(), new HeaderRenderer(), new ImageRenderer(), new ItalicRenderer(), new LaTeXRenderer(), new ListItemRenderer(), new ListRenderer(), new MarkRenderer(), new MathRenderer(), new OmittedRenderer(), new PreRenderer(), new RubyRenderer(), new SmallRenderer(), new SubscriptRenderer(), new SuperscriptRenderer(), new SvgRenderer(), new TableCellRenderer(), new TableRenderer(), new UnderlineRenderer(), new WalkThroughRenderer() }; elRenderers = new HashMap<>(renderers.length); for (ElementRenderer renderer : renderers) { for (String tag : renderer.supportedTags()) { elRenderers.put(tag, renderer); } } this.config = config; // custom tag renderer will overwrite the built-in renderer if (config.getCustomRenderers() != null) { for (ElementRenderer customRenderer : config.getCustomRenderers()) { for (String tag : customRenderer.supportedTags()) { elRenderers.put(tag, customRenderer); } } } } public HtmlRenderConfig getConfig() { return config; } @Override protected boolean validate(String data) { return StringUtils.isNotEmpty(data); } @Override public void doRender(RenderContext context) throws Exception { Document document = JsoupUtils.parse(context.getData()); document.outputSettings().prettyPrint(false).indentAmount(0); HtmlRenderContext htmlRenderContext = new HtmlRenderContext(context, elRenderers::get); htmlRenderContext.setGlobalFont(config.getGlobalFont()); if (config.getGlobalFontSizeInHalfPoints() > 0) { htmlRenderContext.setGlobalFontSize(BigInteger.valueOf(config.getGlobalFontSizeInHalfPoints())); } htmlRenderContext.getNumberingContext().setIndent(config.getNumberingIndent()); htmlRenderContext.getNumberingContext().setHanging(config.getNumberingHanging()); htmlRenderContext.getNumberingContext().setSpacing(config.getNumberingSpacing()); htmlRenderContext.setShowDefaultTableBorderInTableCell(config.isShowDefaultTableBorderInTableCell()); htmlRenderContext.setMathRenderConfig(config.getMathRenderConfig()); htmlRenderContext.renderDocument(document); } @Override protected void afterRender(RenderContext context) { boolean hasSibling = hasSibling(context.getRun()); clearPlaceholder(context, !hasSibling); IBody container = context.getContainer(); if (container.getPartType() == BodyType.TABLECELL) { // 单元格的最后一个元素应为p,否则可能无法正常打开文件 List bodyElements = container.getBodyElements(); if (bodyElements.isEmpty() || bodyElements.get(bodyElements.size() - 1).getElementType() != BodyElementType.PARAGRAPH) { ((XWPFTableCell) container).addParagraph(); } } } private boolean hasSibling(XWPFRun run) { boolean hasSibling = false; CTR ctr = run.getCTR(); XmlCursor xmlCursor = ctr.newCursor(); xmlCursor.push(); while (xmlCursor.toNextSibling()) { if (isValidSibling(xmlCursor)) { hasSibling = true; break; } } if (!hasSibling) { xmlCursor.pop(); while (xmlCursor.toPrevSibling()) { if (isValidSibling(xmlCursor)) { hasSibling = true; break; } } } xmlCursor.dispose(); return hasSibling; } private boolean isValidSibling(XmlCursor cursor) { return !XmlUtils.INVALID_R_SIBLINGS.contains(cursor.getName()); } } ================================================ FILE: src/main/java/org/ddr/poi/html/tag/ARenderer.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.html.tag; import org.ddr.poi.html.ElementRenderer; import org.ddr.poi.html.HtmlConstants; import org.ddr.poi.html.HtmlRenderContext; import org.jsoup.nodes.Element; /** * a标签渲染器 * * @author Draco * @since 2021-03-31 */ public class ARenderer implements ElementRenderer { private static final String[] TAGS = {HtmlConstants.TAG_A}; /** * 开始渲染 * * @param element HTML元素 * @param context 渲染上下文 * @return 是否继续渲染子元素 */ @Override public boolean renderStart(Element element, HtmlRenderContext context) { String href = element.attr(HtmlConstants.ATTR_HREF); context.startHyperlink(href); return true; } /** * 元素渲染结束需要执行的逻辑 * * @param element HTML元素 * @param context 渲染上下文 */ @Override public void renderEnd(Element element, HtmlRenderContext context) { context.endHyperlink(); } @Override public String[] supportedTags() { return TAGS; } @Override public boolean renderAsBlock() { return false; } } ================================================ FILE: src/main/java/org/ddr/poi/html/tag/BigRenderer.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.html.tag; import org.ddr.poi.html.ElementRenderer; import org.ddr.poi.html.HtmlConstants; import org.ddr.poi.html.HtmlRenderContext; import org.ddr.poi.html.util.CSSStyleUtils; import org.jsoup.nodes.Element; /** * big标签渲染器,HTML5不支持big * * @author Draco * @since 2021-02-23 */ @Deprecated public class BigRenderer implements ElementRenderer { private static final String[] TAGS = {HtmlConstants.TAG_BIG}; /** * 开始渲染 * * @param element HTML元素 * @param context 渲染上下文 * @return 是否继续渲染子元素 */ @Override public boolean renderStart(Element element, HtmlRenderContext context) { context.pushInlineStyle(CSSStyleUtils.parse(HtmlConstants.DEFINED_LARGER), element.isBlock()); return true; } /** * 元素渲染结束需要执行的逻辑 * * @param element HTML元素 * @param context 渲染上下文 */ @Override public void renderEnd(Element element, HtmlRenderContext context) { context.popInlineStyle(); } @Override public String[] supportedTags() { return TAGS; } @Override public boolean renderAsBlock() { return false; } } ================================================ FILE: src/main/java/org/ddr/poi/html/tag/BoldRenderer.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.html.tag; import org.ddr.poi.html.ElementRenderer; import org.ddr.poi.html.HtmlConstants; import org.ddr.poi.html.HtmlRenderContext; import org.ddr.poi.html.util.CSSStyleUtils; import org.jsoup.nodes.Element; /** * 加粗标签渲染器 * * @author Draco * @since 2021-02-23 */ public class BoldRenderer implements ElementRenderer { private static final String[] TAGS = {HtmlConstants.TAG_B, HtmlConstants.TAG_STRONG}; /** * 开始渲染 * * @param element HTML元素 * @param context 渲染上下文 * @return 是否继续渲染子元素 */ @Override public boolean renderStart(Element element, HtmlRenderContext context) { context.pushInlineStyle(CSSStyleUtils.parse(HtmlConstants.DEFINED_BOLD), element.isBlock()); return true; } /** * 元素渲染结束需要执行的逻辑 * * @param element HTML元素 * @param context 渲染上下文 */ @Override public void renderEnd(Element element, HtmlRenderContext context) { context.popInlineStyle(); } @Override public String[] supportedTags() { return TAGS; } @Override public boolean renderAsBlock() { return false; } } ================================================ FILE: src/main/java/org/ddr/poi/html/tag/BreakRenderer.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.html.tag; import org.apache.poi.xwpf.usermodel.IRunBody; import org.apache.poi.xwpf.usermodel.XWPFParagraph; import org.ddr.poi.html.ElementRenderer; import org.ddr.poi.html.HtmlConstants; import org.ddr.poi.html.HtmlRenderContext; import org.jsoup.nodes.Element; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR; /** * br标签渲染器 * * @author Draco * @since 2021-02-09 */ public class BreakRenderer implements ElementRenderer { private static final String[] TAGS = {HtmlConstants.TAG_BR}; /** * 开始渲染 * * @param element HTML元素 * @param context 渲染上下文 * @return 是否继续渲染子元素 */ @Override public boolean renderStart(Element element, HtmlRenderContext context) { CTR ctr = context.newRun(); ctr.addNewBr(); IRunBody parent = context.getCurrentRun().getParent(); if (parent instanceof XWPFParagraph) { context.markDedupe((XWPFParagraph) parent); } return false; } @Override public String[] supportedTags() { return TAGS; } @Override public boolean renderAsBlock() { return false; } } ================================================ FILE: src/main/java/org/ddr/poi/html/tag/DeleteRenderer.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.html.tag; import org.ddr.poi.html.ElementRenderer; import org.ddr.poi.html.HtmlConstants; import org.ddr.poi.html.HtmlRenderContext; import org.ddr.poi.html.util.CSSStyleUtils; import org.jsoup.nodes.Element; /** * 删除线标签渲染器 * * @author Draco * @since 2021-02-23 */ public class DeleteRenderer implements ElementRenderer { private static final String[] TAGS = {HtmlConstants.TAG_S, HtmlConstants.TAG_DEL, HtmlConstants.TAG_STRIKE}; /** * 开始渲染 * * @param element HTML元素 * @param context 渲染上下文 * @return 是否继续渲染子元素 */ @Override public boolean renderStart(Element element, HtmlRenderContext context) { context.pushInlineStyle(CSSStyleUtils.parse(HtmlConstants.DEFINED_STRIKE), element.isBlock()); return true; } /** * 元素渲染结束需要执行的逻辑 * * @param element HTML元素 * @param context 渲染上下文 */ @Override public void renderEnd(Element element, HtmlRenderContext context) { context.popInlineStyle(); } @Override public String[] supportedTags() { return TAGS; } @Override public boolean renderAsBlock() { return false; } } ================================================ FILE: src/main/java/org/ddr/poi/html/tag/FigureCaptionRenderer.java ================================================ package org.ddr.poi.html.tag; import org.ddr.poi.html.ElementRenderer; import org.ddr.poi.html.HtmlConstants; import org.ddr.poi.html.HtmlRenderContext; import org.jsoup.nodes.Element; /** * figcaption标签渲染器 * * @author Draco * @since 2022-11-03 */ public class FigureCaptionRenderer implements ElementRenderer { private static final String[] TAGS = {HtmlConstants.TAG_FIGURE_CAPTION}; /** * 开始渲染 * * @param element HTML元素 * @param context 渲染上下文 * @return 是否继续渲染子元素 */ @Override public boolean renderStart(Element element, HtmlRenderContext context) { context.markDedupe(context.getClosestParagraph()); return true; } /** * 元素渲染结束需要执行的逻辑 * * @param element HTML元素 * @param context 渲染上下文 */ @Override public void renderEnd(Element element, HtmlRenderContext context) { context.unmarkDedupe(); } /** * @return 支持的HTML标签 */ @Override public String[] supportedTags() { return TAGS; } /** * @return 是否为块状渲染,如果为true在Word中会另起一个Paragraph */ @Override public boolean renderAsBlock() { return true; } } ================================================ FILE: src/main/java/org/ddr/poi/html/tag/FigureRenderer.java ================================================ package org.ddr.poi.html.tag; import com.steadystate.css.dom.CSSStyleDeclarationImpl; import org.apache.poi.xwpf.usermodel.ParagraphAlignment; import org.apache.poi.xwpf.usermodel.XWPFParagraph; import org.ddr.poi.html.ElementRenderer; import org.ddr.poi.html.HtmlConstants; import org.ddr.poi.html.HtmlRenderContext; import org.ddr.poi.html.util.JsoupUtils; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; /** * figure标签渲染器 * * @author Draco * @since 2022-11-03 */ public class FigureRenderer implements ElementRenderer { private static final String[] TAGS = {HtmlConstants.TAG_FIGURE}; /** * 开始渲染 * * @param element HTML元素 * @param context 渲染上下文 * @return 是否继续渲染子元素 */ @Override public boolean renderStart(Element element, HtmlRenderContext context) { // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/figure#usage_notes Elements captions = JsoupUtils.children(element, HtmlConstants.TAG_FIGURE_CAPTION); if (captions.size() > 1) { captions.remove(0); captions.remove(); } XWPFParagraph paragraph = context.getClosestParagraph(); context.markDedupe(paragraph); CSSStyleDeclarationImpl styleDeclaration = context.currentElementStyle(); String cssFloat = styleDeclaration.getPropertyValue(HtmlConstants.CSS_FLOAT); if (HtmlConstants.LEFT.equals(cssFloat)) { paragraph.setAlignment(ParagraphAlignment.LEFT); styleDeclaration.setTextAlign(HtmlConstants.LEFT); } else if (HtmlConstants.RIGHT.equals(cssFloat)) { paragraph.setAlignment(ParagraphAlignment.RIGHT); styleDeclaration.setTextAlign(HtmlConstants.RIGHT); } else { paragraph.setAlignment(ParagraphAlignment.CENTER); styleDeclaration.setTextAlign(HtmlConstants.CENTER); } return true; } /** * 元素渲染结束需要执行的逻辑 * * @param element HTML元素 * @param context 渲染上下文 */ @Override public void renderEnd(Element element, HtmlRenderContext context) { context.unmarkDedupe(); } /** * @return 支持的HTML标签 */ @Override public String[] supportedTags() { return TAGS; } /** * @return 是否为块状渲染,如果为true在Word中会另起一个Paragraph */ @Override public boolean renderAsBlock() { return true; } } ================================================ FILE: src/main/java/org/ddr/poi/html/tag/HeaderBreakRenderer.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.html.tag; import org.ddr.poi.html.ElementRenderer; import org.ddr.poi.html.HtmlConstants; import org.ddr.poi.html.HtmlRenderContext; import org.ddr.poi.html.util.RenderUtils; import org.jsoup.nodes.Element; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBorder; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTP; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPBdr; import org.openxmlformats.schemas.wordprocessingml.x2006.main.STBorder; import java.math.BigInteger; /** * hr标签渲染器 * * @author Draco * @since 2021-02-18 */ public class HeaderBreakRenderer implements ElementRenderer { private static final String[] TAGS = {HtmlConstants.TAG_HR}; /** * 线粗细,相当于3px */ private static final BigInteger SIZE = BigInteger.valueOf(6); /** * 间距 */ private static final BigInteger SPACE = BigInteger.ONE; /** * 开始渲染 * * @param element HTML元素 * @param context 渲染上下文 * @return 是否继续渲染子元素 */ @Override public boolean renderStart(Element element, HtmlRenderContext context) { CTP ctp = context.getClosestParagraph().getCTP(); CTPBdr pBdr = RenderUtils.getPBdr(RenderUtils.getPPr(ctp)); CTBorder ctBorder = pBdr.addNewBottom(); ctBorder.setVal(STBorder.SINGLE); ctBorder.setSz(SIZE); ctBorder.setSpace(SPACE); ctBorder = pBdr.addNewBetween(); ctBorder.setVal(STBorder.SINGLE); ctBorder.setSz(SIZE); ctBorder.setSpace(SPACE); return false; } @Override public void renderEnd(Element element, HtmlRenderContext context) { context.unmarkDedupe(); } @Override public String[] supportedTags() { return TAGS; } @Override public boolean renderAsBlock() { return true; } } ================================================ FILE: src/main/java/org/ddr/poi/html/tag/HeaderRenderer.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.html.tag; import org.ddr.poi.html.ElementRenderer; import org.ddr.poi.html.HtmlConstants; import org.ddr.poi.html.HtmlRenderContext; import org.ddr.poi.html.util.CSSStyleUtils; import org.ddr.poi.html.util.RenderUtils; import org.jsoup.nodes.Element; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTDecimalNumber; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTP; import java.math.BigInteger; /** * h1~h6标签渲染器 * * @author Draco * @since 2021-02-24 */ public class HeaderRenderer implements ElementRenderer { private static final String[] TAGS = { HtmlConstants.TAG_H1, HtmlConstants.TAG_H2, HtmlConstants.TAG_H3, HtmlConstants.TAG_H4, HtmlConstants.TAG_H5, HtmlConstants.TAG_H6 }; /** * 各级别标题对应字号 */ private static final String[] FONT_SIZES = {"24pt", "18pt", "14pt", "12pt", "10pt", "7.5pt"}; /** * 开始渲染 * * @param element HTML元素 * @param context 渲染上下文 * @return 是否继续渲染子元素 */ @Override public boolean renderStart(Element element, HtmlRenderContext context) { int index = Integer.parseInt(element.normalName().substring(1)) - 1; String fontSizeStyle = HtmlConstants.inlineStyle(HtmlConstants.CSS_FONT_SIZE, FONT_SIZES[index]); context.pushInlineStyle(CSSStyleUtils.parse(HtmlConstants.DEFINED_BOLD + fontSizeStyle), element.isBlock()); CTP ctp = context.getClosestParagraph().getCTP(); CTDecimalNumber ctDecimalNumber = CTDecimalNumber.Factory.newInstance(); ctDecimalNumber.setVal(BigInteger.valueOf(index)); RenderUtils.getPPr(ctp).setOutlineLvl(ctDecimalNumber); return true; } /** * 元素渲染结束需要执行的逻辑 * * @param element HTML元素 * @param context 渲染上下文 */ @Override public void renderEnd(Element element, HtmlRenderContext context) { context.popInlineStyle(); } @Override public String[] supportedTags() { return TAGS; } @Override public boolean renderAsBlock() { return true; } } ================================================ FILE: src/main/java/org/ddr/poi/html/tag/ImageRenderer.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.html.tag; import com.drew.imaging.FileType; import com.drew.imaging.FileTypeDetector; import com.drew.imaging.ImageMetadataReader; import com.drew.imaging.ImageProcessingException; import com.drew.metadata.Metadata; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.math.NumberUtils; import org.apache.poi.openxml4j.exceptions.InvalidFormatException; import org.apache.poi.util.Units; import org.apache.poi.xwpf.usermodel.SVGPictureData; import org.ddr.image.ImageInfo; import org.ddr.image.ImageType; import org.ddr.image.MetadataReader; import org.ddr.image.MetadataReaders; import org.ddr.poi.html.ElementRenderer; import org.ddr.poi.html.HtmlConstants; import org.ddr.poi.html.HtmlRenderContext; import org.ddr.poi.html.util.CSSLength; import org.ddr.poi.math.MathMLUtils; import org.ddr.poi.util.ByteArrayCopyStream; import org.ddr.poi.util.HttpURLConnectionUtils; import org.jsoup.nodes.Element; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.imageio.ImageIO; import javax.imageio.ImageReader; import java.awt.*; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.Iterator; /** * img标签渲染器 * * @author Draco * @since 2021-02-09 */ public class ImageRenderer implements ElementRenderer { private static final Logger log = LoggerFactory.getLogger(ImageRenderer.class); private static final String[] TAGS = {HtmlConstants.TAG_IMG}; private static final String HTTP = "http"; private static final String DOUBLE_SLASH = "//"; private static final String DATA_PREFIX = "data:"; private static final String COMMENT_MATH_PREFIX = ""; static { SVGPictureData.initRelation(); } /** * 元素渲染结束需要执行的逻辑 * * @param element HTML元素 * @param context 渲染上下文 */ @Override public boolean renderStart(Element element, HtmlRenderContext context) { String src = element.attr(HtmlConstants.ATTR_SRC); if (StringUtils.startsWithIgnoreCase(src, HTTP)) { handleRemoteImage(element, context, src); } else if (StringUtils.startsWith(src, DOUBLE_SLASH)) { // 某些图片链接为了跟随网站协议而隐去了协议名称 handleRemoteImage(element, context, HTTP + HtmlConstants.COLON + src); } else if (StringUtils.startsWith(src, DATA_PREFIX)) { handleData(element, context, src); } return false; } /** * 处理Data URL * * @param element HTML元素 * @param context 渲染上下文 * @param src 数据 */ private void handleData(Element element, HtmlRenderContext context, String src) { int index = src.indexOf(HtmlConstants.COMMA.charAt(0)); String data = src.substring(index + 1); String declaration = src.substring(0, index); String format = StringUtils.substringBetween(declaration, HtmlConstants.SLASH, HtmlConstants.SEMICOLON); // org.apache.poi.sl.usermodel.PictureData.PictureType if (format.contains(HtmlConstants.MINUS)) { format = StringUtils.substringAfterLast(format, HtmlConstants.MINUS); } else if (format.contains(HtmlConstants.PLUS)) { format = StringUtils.substringBefore(format, HtmlConstants.PLUS); } byte[] bytes; if (declaration.contains("base64")) { try { bytes = Base64.getDecoder().decode(data); } catch (Exception e) { log.warn("Failed to load image due to illegal base64 data: {}", src); return; } } else { if (data.startsWith(HtmlConstants.PERCENT)) { try { data = URLDecoder.decode(data, StandardCharsets.UTF_8.name()); } catch (UnsupportedEncodingException e) { log.warn("Failed to load image due to illegal data url: {}", src); return; } } // wiris support int startOfMath = data.indexOf(COMMENT_MATH_PREFIX); if (startOfMath >= 0) { try { int endOfMath = data.indexOf(COMMENT_MATH_SUFFIX, startOfMath + COMMENT_MATH_PREFIX.length()); String math = data.substring(startOfMath + 12, endOfMath + 7); MathMLUtils.renderTo(context.getClosestParagraph(), context.newRun(), math, context.getMathRenderConfig()); return; } catch (Exception e) { log.warn("Failed to render math in wiris svg, will try to render as svg image: {}", data, e); } } bytes = data.getBytes(StandardCharsets.UTF_8); } boolean svg = HtmlConstants.TAG_SVG.equals(format); try (ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes)) { ImageInfo info = analyzeImage(inputStream, svg); if (info == null) { log.warn("Illegal image url: {}", src); return; } addPicture(element, context, info.getStream(), info.getRawType(), info.getWidth(), info.getHeight(), svg ? bytes : null); } catch (IOException | InvalidFormatException e) { log.warn("Failed to load image: {}", src, e); } } /** * 根据图片反推类型 * * @param image 图片 * @return 图片类型 */ protected ImageType typeOf(BufferedImage image) { return image.getColorModel().hasAlpha() ? ImageType.PNG : ImageType.JPG; } /** * 处理远程图片 * * @param element HTML元素 * @param context 渲染上下文 * @param src 图片链接地址 */ private void handleRemoteImage(Element element, HtmlRenderContext context, String src) { HttpURLConnection connect = null; try { connect = HttpURLConnectionUtils.connect(src); HttpURLConnectionUtils.initUserAgent(connect); int firstSlashPosition = src.indexOf('/', src.indexOf("://") + 3); connect.setRequestProperty("Referrer", src.substring(0, firstSlashPosition)); InputStream urlStream = connect.getInputStream(); boolean svg = StringUtils.contains(connect.getHeaderField("content-type"), HtmlConstants.TAG_SVG); ByteArrayCopyStream outputStream = new ByteArrayCopyStream(urlStream.available()); IOUtils.copy(urlStream, outputStream); final byte[] svgData = svg ? outputStream.toByteArray() : null; ByteArrayInputStream inputStream = outputStream.toInput(); ImageInfo info = analyzeImage(inputStream, svg); if (info == null) { log.warn("Illegal image url: {}", src); return; } addPicture(element, context, info.getStream(), info.getRawType(), info.getWidth(), info.getHeight(), svgData); } catch (IOException | InvalidFormatException e) { log.warn("Failed to load image: {}", src, e); } finally { IOUtils.close(connect); } } private ImageInfo analyzeImage(ByteArrayInputStream inputStream, boolean svg) throws IOException, InvalidFormatException { final long length = inputStream.available(); // actual image data stream ByteArrayInputStream stream = inputStream; ImageType type = null; Dimension dimension = null; if (svg) { BufferedImage image = ImageIO.read(inputStream); inputStream.reset(); type = typeOf(image); ByteArrayCopyStream imageStream = new ByteArrayCopyStream(image.getData().getDataBuffer().getSize()); ImageIO.write(image, type.getExtension(), imageStream); stream = imageStream.toInput(); dimension = new Dimension(image.getWidth(), image.getHeight()); } else { FileType fileType = FileTypeDetector.detectFileType(inputStream); for (MetadataReader metadataReader : MetadataReaders.INSTANCES) { if (metadataReader.canRead(fileType)) { try { // FIXME metadata-extractor 一直未发版支持 AVIF 格式,会被归为 QuickTime 格式 if (fileType == FileType.QuickTime) { fileType = FileType.Heif; } Metadata metadata = ImageMetadataReader.readMetadata(inputStream, length, fileType); type = metadataReader.getType(metadata); dimension = metadataReader.getDimension(metadata); break; } catch (ImageProcessingException ignored) { } } } inputStream.reset(); if (dimension == null) { Iterator imageReaders = ImageIO.getImageReaders(inputStream); while (imageReaders.hasNext()) { ImageReader reader = imageReaders.next(); try { dimension = new Dimension(reader.getWidth(0), reader.getHeight(0)); break; } catch (IOException ignored) { } } if (dimension == null) { BufferedImage image = ImageIO.read(inputStream); inputStream.reset(); if (image == null) { return null; } if (type == null) { type = typeOf(image); } dimension = new Dimension(image.getWidth(), image.getHeight()); } } } return new ImageInfo(stream, type, dimension); } @Override public String[] supportedTags() { return TAGS; } @Override public boolean renderAsBlock() { return false; } /** * 添加图片 * * @param element HTML元素 * @param context 渲染上下文 * @param inputStream 图片数据流 * @param type 图片类型 * @param widthInPixels 图片宽度(像素) * @param heightInPixels 图片高度(像素) * @param svgData SVG数据 */ protected void addPicture(Element element, HtmlRenderContext context, InputStream inputStream, int type, int widthInPixels, int heightInPixels, byte[] svgData) throws InvalidFormatException, IOException { // 容器限制宽度 int containerWidth = context.getAvailableWidthInEMU(); // int containerHeight = context.getAvailablePageHeight(); // 图片原始宽高 int widthInEMU = Units.pixelToEMU(widthInPixels); int heightInEMU = Units.pixelToEMU(heightInPixels); float naturalAspect = 1f * widthInEMU / heightInEMU; int declaredWidth = widthInEMU; int declaredHeight = heightInEMU; int maxWidthInEMU = containerWidth; int maxHeightInEMU = Integer.MAX_VALUE; String width = context.getPropertyValue(HtmlConstants.CSS_WIDTH); if (width.length() > 0) { CSSLength cssLength = CSSLength.of(width); if (cssLength.isValid()) { declaredWidth = context.computeLengthInEMU(cssLength, widthInEMU, containerWidth); } } else { // width attribute is overridden by style, the same to height // https://css-tricks.com/whats-the-difference-between-width-height-in-css-and-width-height-html-attributes/ width = element.attr(HtmlConstants.ATTR_WIDTH); if (NumberUtils.isParsable(width)) { width += HtmlConstants.PX; CSSLength cssLength = CSSLength.of(width); declaredWidth = context.computeLengthInEMU(cssLength, widthInEMU, containerWidth); } } String maxWidth = context.getPropertyValue(HtmlConstants.CSS_MAX_WIDTH); if (maxWidth.length() > 0) { CSSLength cssLength = CSSLength.of(maxWidth); if (cssLength.isValid()) { // restrained by container maxWidthInEMU = Math.min(context.computeLengthInEMU(cssLength, widthInEMU, containerWidth), containerWidth); } } String height = context.getPropertyValue(HtmlConstants.CSS_HEIGHT); if (height.length() > 0) { CSSLength cssLength = CSSLength.of(height); if (cssLength.isValid()) { declaredHeight = context.computeLengthInEMU(cssLength, heightInEMU, Integer.MAX_VALUE); } } else { height = element.attr(HtmlConstants.ATTR_HEIGHT); if (NumberUtils.isParsable(height)) { height += HtmlConstants.PX; CSSLength cssLength = CSSLength.of(height); declaredHeight = context.computeLengthInEMU(cssLength, heightInEMU, Integer.MAX_VALUE); } } String maxHeight = context.getPropertyValue(HtmlConstants.CSS_MAX_HEIGHT); if (maxHeight.length() > 0) { CSSLength cssLength = CSSLength.of(maxHeight); if (cssLength.isValid()) { maxHeightInEMU = context.computeLengthInEMU(cssLength, heightInEMU, Integer.MAX_VALUE); } } if (declaredWidth == widthInEMU ^ declaredHeight == heightInEMU) { if (declaredWidth == widthInEMU) { declaredWidth = (int) (declaredHeight * naturalAspect); } else { declaredHeight = (int) (declaredWidth / naturalAspect); } } // 计算尺寸 int calculatedWidth, calculatedHeight; if (declaredWidth < maxWidthInEMU && declaredHeight <= maxHeightInEMU) { calculatedWidth = declaredWidth; calculatedHeight = declaredHeight; } else if (declaredWidth > maxWidthInEMU && declaredHeight <= maxHeightInEMU) { calculatedWidth = maxWidthInEMU; calculatedHeight = (int) (maxWidthInEMU / naturalAspect); } else if (declaredHeight > maxHeightInEMU && declaredWidth <= maxWidthInEMU) { calculatedHeight = maxHeightInEMU; calculatedWidth = (int) (maxHeightInEMU * naturalAspect); } else { float widthRatio = 1f * maxWidthInEMU / declaredWidth; float heightRatio = 1f * maxHeightInEMU / declaredHeight; float scale = Math.min(widthRatio, heightRatio); calculatedWidth = (int) (declaredWidth * scale); calculatedHeight = (int) (declaredHeight * scale); } context.renderPicture(inputStream, type, HtmlConstants.TAG_IMG, calculatedWidth, calculatedHeight, svgData); } } ================================================ FILE: src/main/java/org/ddr/poi/html/tag/ItalicRenderer.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.html.tag; import org.ddr.poi.html.ElementRenderer; import org.ddr.poi.html.HtmlConstants; import org.ddr.poi.html.HtmlRenderContext; import org.ddr.poi.html.util.CSSStyleUtils; import org.jsoup.nodes.Element; /** * 斜体标签渲染器 * * @author Draco * @since 2021-02-23 */ public class ItalicRenderer implements ElementRenderer { private static final String[] TAGS = {HtmlConstants.TAG_I, HtmlConstants.TAG_EM}; /** * 开始渲染 * * @param element HTML元素 * @param context 渲染上下文 * @return 是否继续渲染子元素 */ @Override public boolean renderStart(Element element, HtmlRenderContext context) { context.pushInlineStyle(CSSStyleUtils.parse(HtmlConstants.DEFINED_ITALIC), element.isBlock()); return true; } /** * 元素渲染结束需要执行的逻辑 * * @param element HTML元素 * @param context 渲染上下文 */ @Override public void renderEnd(Element element, HtmlRenderContext context) { context.popInlineStyle(); } @Override public String[] supportedTags() { return TAGS; } @Override public boolean renderAsBlock() { return false; } } ================================================ FILE: src/main/java/org/ddr/poi/html/tag/LaTeXRenderer.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.html.tag; import org.apache.commons.lang3.StringUtils; import org.ddr.poi.html.ElementRenderer; import org.ddr.poi.html.HtmlConstants; import org.ddr.poi.html.HtmlRenderContext; import org.ddr.poi.latex.LaTeXUtils; import org.jsoup.nodes.Element; import uk.ac.ed.ph.snuggletex.SnuggleSession; /** * latex标签渲染器(自定义) * * @author Draco * @since 2023-07-17 */ public class LaTeXRenderer implements ElementRenderer { private static final String[] TAGS = {HtmlConstants.TAG_LATEX}; /** * 开始渲染 * * @param element HTML元素 * @param context 渲染上下文 * @return 是否继续渲染子元素 */ @Override public boolean renderStart(Element element, HtmlRenderContext context) { String latex = element.wholeText(); if (StringUtils.isBlank(latex)) { return false; } SnuggleSession session = LaTeXUtils.createSession(); LaTeXUtils.parse(session, latex); LaTeXUtils.renderTo(context.getClosestParagraph(), null, session, context.getMathRenderConfig()); return false; } @Override public String[] supportedTags() { return TAGS; } @Override public boolean renderAsBlock() { return false; } } ================================================ FILE: src/main/java/org/ddr/poi/html/tag/ListItemRenderer.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.html.tag; import org.apache.poi.xwpf.usermodel.XWPFParagraph; import org.ddr.poi.html.ElementRenderer; import org.ddr.poi.html.HtmlConstants; import org.ddr.poi.html.HtmlRenderContext; import org.jsoup.nodes.Element; /** * 列表项渲染器 * * @author Draco * @since 2021-02-19 */ public class ListItemRenderer implements ElementRenderer { private static final String[] TAGS = {HtmlConstants.TAG_LI}; /** * 开始渲染 * * @param element HTML元素 * @param context 渲染上下文 * @return 是否继续渲染子元素 */ @Override public boolean renderStart(Element element, HtmlRenderContext context) { XWPFParagraph paragraph = context.getClosestParagraph(); context.markDedupe(paragraph); context.getNumberingContext().add(paragraph); return true; } /** * 元素渲染结束需要执行的逻辑 * * @param element HTML元素 * @param context 渲染上下文 */ @Override public void renderEnd(Element element, HtmlRenderContext context) { context.unmarkDedupe(); } @Override public String[] supportedTags() { return TAGS; } @Override public boolean renderAsBlock() { return true; } } ================================================ FILE: src/main/java/org/ddr/poi/html/tag/ListRenderer.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.html.tag; import org.apache.commons.lang3.StringUtils; import org.ddr.poi.html.ElementRenderer; import org.ddr.poi.html.HtmlConstants; import org.ddr.poi.html.HtmlRenderContext; import org.ddr.poi.html.util.CSSLength; import org.ddr.poi.html.util.ListStyle; import org.ddr.poi.html.util.ListStyleType; import org.ddr.poi.html.util.RenderUtils; import org.jsoup.nodes.Element; /** * 列表渲染器 * * @author Draco * @since 2021-02-18 */ public class ListRenderer implements ElementRenderer { private static final String[] TAGS = {HtmlConstants.TAG_UL, HtmlConstants.TAG_OL}; /** * 开始渲染 * * @param element HTML元素 * @param context 渲染上下文 * @return 是否继续渲染子元素 */ @Override public boolean renderStart(Element element, HtmlRenderContext context) { String listStylePosition = context.currentElementStyle().getPropertyValue(HtmlConstants.CSS_LIST_STYLE_POSITION); boolean hanging = !HtmlConstants.INSIDE.equals(listStylePosition); CSSLength marginLeft = CSSLength.of(context.currentElementStyle().getMarginLeft().toLowerCase()); int left = marginLeft.isValid() && !marginLeft.isPercent() ? RenderUtils.emuToTwips(context.lengthToEMU(marginLeft)) : 0; CSSLength marginRight = CSSLength.of(context.currentElementStyle().getMarginRight().toLowerCase()); int right = marginRight.isValid() && !marginRight.isPercent() ? RenderUtils.emuToTwips(context.lengthToEMU(marginRight)) : 0; ListStyle listStyle = new ListStyle(determineNumberFormat(context, element), hanging, left, right); context.getNumberingContext().startLevel(listStyle); return true; } private ListStyleType determineNumberFormat(HtmlRenderContext context, Element element) { String listStyleType = context.currentElementStyle() .getPropertyValue(HtmlConstants.CSS_LIST_STYLE_TYPE).toLowerCase(); ListStyleType format; switch (element.tag().normalName()) { case HtmlConstants.TAG_OL: if (StringUtils.isNotBlank(listStyleType)) { format = ListStyleType.Ordered.of(listStyleType); } else { // 支持ol的type属性 String type = element.attr(HtmlConstants.ATTR_TYPE); format = ListStyleType.Ordered.of(type); } break; case HtmlConstants.TAG_UL: format = ListStyleType.Unordered.of(listStyleType); break; default: format = ListStyleType.Unordered.NONE; } return format; } /** * 元素渲染结束需要执行的逻辑 * * @param element HTML元素 * @param context 渲染上下文 */ @Override public void renderEnd(Element element, HtmlRenderContext context) { context.getNumberingContext().endLevel(); } @Override public String[] supportedTags() { return TAGS; } @Override public boolean renderAsBlock() { // 列表标签本身不需要作为块状元素渲染,因为每一个列表项都是一个块状元素 return false; } } ================================================ FILE: src/main/java/org/ddr/poi/html/tag/MarkRenderer.java ================================================ /* * Copyright 2016 - 2022 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.html.tag; import com.steadystate.css.dom.CSSStyleDeclarationImpl; import org.apache.commons.lang3.StringUtils; import org.ddr.poi.html.ElementRenderer; import org.ddr.poi.html.HtmlConstants; import org.ddr.poi.html.HtmlRenderContext; import org.jsoup.nodes.Element; /** * 标记标签渲染器 * * @author Draco * @since 2022-06-11 */ public class MarkRenderer implements ElementRenderer { private static final String[] TAGS = {HtmlConstants.TAG_MARK}; /** * 开始渲染 * * @param element HTML元素 * @param context 渲染上下文 * @return 是否继续渲染子元素 */ @Override public boolean renderStart(Element element, HtmlRenderContext context) { CSSStyleDeclarationImpl cssStyleDeclaration = context.currentElementStyle(); if (StringUtils.isEmpty(cssStyleDeclaration.getBackgroundColor())) { cssStyleDeclaration.setBackgroundColor("yellow"); } return true; } @Override public String[] supportedTags() { return TAGS; } @Override public boolean renderAsBlock() { return false; } } ================================================ FILE: src/main/java/org/ddr/poi/html/tag/MathRenderer.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.html.tag; import org.ddr.poi.html.ElementRenderer; import org.ddr.poi.html.HtmlConstants; import org.ddr.poi.html.HtmlRenderContext; import org.ddr.poi.math.MathMLUtils; import org.jsoup.nodes.Document.OutputSettings; import org.jsoup.nodes.Document.OutputSettings.Syntax; import org.jsoup.nodes.Element; /** * math标签渲染器 * * @author Draco * @since 2021-02-18 */ public class MathRenderer implements ElementRenderer { private static final String[] TAGS = {HtmlConstants.TAG_MATH}; /** * 开始渲染 * * @param element HTML元素 * @param context 渲染上下文 * @return 是否继续渲染子元素 */ @Override public boolean renderStart(Element element, HtmlRenderContext context) { OutputSettings outputSettings = element.ownerDocument().outputSettings(); outputSettings.syntax(Syntax.xml); String math = element.outerHtml(); outputSettings.syntax(Syntax.html); math = MathMLUtils.normalize(math); MathMLUtils.renderTo(context.getClosestParagraph(), context.newRun(), math, context.getMathRenderConfig()); return false; } @Override public String[] supportedTags() { return TAGS; } @Override public boolean renderAsBlock() { return false; } } ================================================ FILE: src/main/java/org/ddr/poi/html/tag/OmittedRenderer.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.html.tag; import org.ddr.poi.html.ElementRenderer; import org.ddr.poi.html.HtmlConstants; import org.ddr.poi.html.HtmlRenderContext; import org.jsoup.nodes.Element; /** * 直接忽略的元素渲染器 * * @author Draco * @since 2021-03-15 */ public class OmittedRenderer implements ElementRenderer { private static final String[] TAGS = { HtmlConstants.TAG_HEAD, HtmlConstants.TAG_SCRIPT, HtmlConstants.TAG_NOSCRIPT, HtmlConstants.TAG_FRAME, HtmlConstants.TAG_FRAMESET, HtmlConstants.TAG_IFRAME, HtmlConstants.TAG_NOFRAMES, HtmlConstants.TAG_COLGROUP, HtmlConstants.TAG_COL, HtmlConstants.TAG_TEMPLATE, HtmlConstants.TAG_RP, }; /** * 开始渲染 * * @param element HTML元素 * @param context 渲染上下文 * @return 是否继续渲染子元素 */ @Override public boolean renderStart(Element element, HtmlRenderContext context) { return false; } @Override public String[] supportedTags() { return TAGS; } @Override public boolean renderAsBlock() { return false; } } ================================================ FILE: src/main/java/org/ddr/poi/html/tag/PreRenderer.java ================================================ package org.ddr.poi.html.tag; import org.ddr.poi.html.ElementRenderer; import org.ddr.poi.html.HtmlConstants; import org.ddr.poi.html.HtmlRenderContext; import org.ddr.poi.html.util.CSSStyleUtils; import org.jsoup.nodes.Element; /** * pre标签渲染器 * * @author Draco * @since 2023-06-25 */ public class PreRenderer implements ElementRenderer { private static final String[] TAGS = {HtmlConstants.TAG_PRE, HtmlConstants.TAG_XMP}; @Override public boolean renderStart(Element element, HtmlRenderContext context) { context.pushInlineStyle(CSSStyleUtils.parse(HtmlConstants.DEFINED_PRE), element.isBlock()); return true; } @Override public void renderEnd(Element element, HtmlRenderContext context) { context.popInlineStyle(); } @Override public String[] supportedTags() { return TAGS; } @Override public boolean renderAsBlock() { return true; } } ================================================ FILE: src/main/java/org/ddr/poi/html/tag/RubyRenderer.java ================================================ /* * Copyright 2016 - 2022 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.html.tag; import org.apache.commons.lang3.StringUtils; import org.apache.xmlbeans.impl.xb.xmlschema.SpaceAttribute; import org.ddr.poi.html.ElementRenderer; import org.ddr.poi.html.HtmlConstants; import org.ddr.poi.html.HtmlRenderContext; import org.ddr.poi.html.util.RenderUtils; import org.jsoup.internal.StringUtil; import org.jsoup.nodes.Element; import org.jsoup.nodes.Node; import org.jsoup.nodes.TextNode; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTRPr; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTText; import org.openxmlformats.schemas.wordprocessingml.x2006.main.STFldCharType; /** * ruby标签渲染器 * * @author Draco * @since 2022-06-12 20:47 */ public class RubyRenderer implements ElementRenderer { private static final String[] TAGS = { HtmlConstants.TAG_RUBY, }; @Override public boolean renderStart(Element element, HtmlRenderContext context) { StringBuilder sb = StringUtil.borrowBuilder(); for (Node childNode : element.childNodes()) { if (childNode instanceof Element) { String tagName = ((Element) childNode).normalName(); if (HtmlConstants.TAG_RT.equals(tagName)) { String rt = ((Element) childNode).text(); CTR ctr = context.newRun(); CTRPr rPr = RenderUtils.getRPr(ctr); rPr.addNewLang().setVal("en-US"); ctr.addNewFldChar().setFldCharType(STFldCharType.BEGIN); ctr = context.newRun(); rPr = RenderUtils.getRPr(ctr); rPr.addNewLang().setVal("en-US"); CTText ctText = ctr.addNewInstrText(); ctText.setSpace(SpaceAttribute.Space.PRESERVE); int fontSize = context.getGlobalFontSize() == null ? context.getInheritedFontSizeInHalfPoints() : context.getGlobalFontSize().intValue(); fontSize = (fontSize + 1) / 2; ctText.setStringValue("EQ \\* jc0 \\* hps" + fontSize + " \\o \\ad(\\s \\up " + (fontSize - 1) + "(" + rt + ")," + StringUtil.releaseBuilder(sb).trim() + ")"); sb = StringUtil.borrowBuilder(); ctr = context.newRun(); rPr = RenderUtils.getRPr(ctr); rPr.addNewLang().setVal("en-US"); ctr.addNewFldChar().setFldCharType(STFldCharType.END); } else if (HtmlConstants.TAG_RP.equals(tagName)) { continue; } else { StringUtil.appendNormalisedWhitespace(sb, ((Element) childNode).wholeText(), false); } } else if (childNode instanceof TextNode) { StringUtil.appendNormalisedWhitespace(sb, ((TextNode) childNode).getWholeText(), false); } } String remainText = StringUtil.releaseBuilder(sb); if (StringUtils.isNotBlank(remainText)) { context.renderText(remainText); } return false; } @Override public String[] supportedTags() { return TAGS; } @Override public boolean renderAsBlock() { return false; } } ================================================ FILE: src/main/java/org/ddr/poi/html/tag/SmallRenderer.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.html.tag; import org.ddr.poi.html.ElementRenderer; import org.ddr.poi.html.HtmlConstants; import org.ddr.poi.html.HtmlRenderContext; import org.ddr.poi.html.util.CSSStyleUtils; import org.jsoup.nodes.Element; /** * small标签渲染器 * * @author Draco * @since 2021-02-23 */ public class SmallRenderer implements ElementRenderer { private static final String[] TAGS = {HtmlConstants.TAG_SMALL}; /** * 开始渲染 * * @param element HTML元素 * @param context 渲染上下文 * @return 是否继续渲染子元素 */ @Override public boolean renderStart(Element element, HtmlRenderContext context) { context.pushInlineStyle(CSSStyleUtils.parse(HtmlConstants.DEFINED_SMALLER), element.isBlock()); return true; } /** * 元素渲染结束需要执行的逻辑 * * @param element HTML元素 * @param context 渲染上下文 */ @Override public void renderEnd(Element element, HtmlRenderContext context) { context.popInlineStyle(); } @Override public String[] supportedTags() { return TAGS; } @Override public boolean renderAsBlock() { return false; } } ================================================ FILE: src/main/java/org/ddr/poi/html/tag/SubscriptRenderer.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.html.tag; import org.ddr.poi.html.ElementRenderer; import org.ddr.poi.html.HtmlConstants; import org.ddr.poi.html.HtmlRenderContext; import org.ddr.poi.html.util.CSSStyleUtils; import org.jsoup.nodes.Element; /** * sub标签渲染器 * * @author Draco * @since 2021-02-24 */ public class SubscriptRenderer implements ElementRenderer { private static final String[] TAGS = {HtmlConstants.TAG_SUB}; /** * 开始渲染 * * @param element HTML元素 * @param context 渲染上下文 * @return 是否继续渲染子元素 */ @Override public boolean renderStart(Element element, HtmlRenderContext context) { context.pushInlineStyle(CSSStyleUtils.parse(HtmlConstants.DEFINED_SUBSCRIPT), element.isBlock()); return true; } /** * 元素渲染结束需要执行的逻辑 * * @param element HTML元素 * @param context 渲染上下文 */ @Override public void renderEnd(Element element, HtmlRenderContext context) { context.popInlineStyle(); } @Override public String[] supportedTags() { return TAGS; } @Override public boolean renderAsBlock() { return false; } } ================================================ FILE: src/main/java/org/ddr/poi/html/tag/SuperscriptRenderer.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.html.tag; import org.ddr.poi.html.ElementRenderer; import org.ddr.poi.html.HtmlConstants; import org.ddr.poi.html.HtmlRenderContext; import org.ddr.poi.html.util.CSSStyleUtils; import org.jsoup.nodes.Element; /** * sup标签渲染器 * * @author Draco * @since 2021-02-24 */ public class SuperscriptRenderer implements ElementRenderer { private static final String[] TAGS = {HtmlConstants.TAG_SUP}; /** * 开始渲染 * * @param element HTML元素 * @param context 渲染上下文 * @return 是否继续渲染子元素 */ @Override public boolean renderStart(Element element, HtmlRenderContext context) { context.pushInlineStyle(CSSStyleUtils.parse(HtmlConstants.DEFINED_SUPERSCRIPT), element.isBlock()); return true; } /** * 元素渲染结束需要执行的逻辑 * * @param element HTML元素 * @param context 渲染上下文 */ @Override public void renderEnd(Element element, HtmlRenderContext context) { context.popInlineStyle(); } @Override public String[] supportedTags() { return TAGS; } @Override public boolean renderAsBlock() { return false; } } ================================================ FILE: src/main/java/org/ddr/poi/html/tag/SvgRenderer.java ================================================ package org.ddr.poi.html.tag; import org.apache.poi.openxml4j.exceptions.InvalidFormatException; import org.ddr.image.ImageType; import org.ddr.poi.html.HtmlConstants; import org.ddr.poi.html.HtmlRenderContext; import org.ddr.poi.util.ByteArrayCopyStream; import org.jsoup.nodes.Element; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; /** * svg标签渲染器 * * @author Draco * @since 2022-04-13 */ public class SvgRenderer extends ImageRenderer { private static final Logger log = LoggerFactory.getLogger(SvgRenderer.class); private static final String[] TAGS = {HtmlConstants.TAG_SVG}; static { System.setProperty("com.twelvemonkeys.imageio.plugins.svg.allowExternalResources", "true"); } /** * 开始渲染 * * @param element HTML元素 * @param context 渲染上下文 * @return 是否继续渲染子元素 */ @Override public boolean renderStart(Element element, HtmlRenderContext context) { if (!element.hasAttr("xmlns")) { element.attr("xmlns", "http://www.w3.org/2000/svg"); } String svg = element.outerHtml().replace(" />", "/>"); byte[] bytes = svg.getBytes(StandardCharsets.UTF_8); BufferedImage image; try (InputStream svgStream = new ByteArrayInputStream(bytes)) { image = ImageIO.read(svgStream); ImageType type = typeOf(image); int size = image.getData().getDataBuffer().getSize(); ByteArrayCopyStream outputStream = new ByteArrayCopyStream(size); ImageIO.write(image, type.getExtension(), outputStream); InputStream imageStream = outputStream.toInput(); addPicture(element, context, imageStream, type.getType(), image.getWidth(), image.getHeight(), bytes); } catch (IOException | InvalidFormatException e) { log.warn("Failed to render svg as image: {}", svg, e); } finally { // 释放资源 image = null; } return false; } /** * @return 支持的HTML标签 */ @Override public String[] supportedTags() { return TAGS; } } ================================================ FILE: src/main/java/org/ddr/poi/html/tag/TableCellRenderer.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.html.tag; import com.steadystate.css.dom.CSSStyleDeclarationImpl; import org.apache.commons.lang3.math.NumberUtils; import org.apache.poi.xwpf.usermodel.XWPFParagraph; import org.apache.poi.xwpf.usermodel.XWPFTable; import org.apache.poi.xwpf.usermodel.XWPFTableCell; import org.apache.xmlbeans.XmlCursor; import org.ddr.poi.html.ElementRenderer; import org.ddr.poi.html.HtmlConstants; import org.ddr.poi.html.HtmlRenderContext; import org.ddr.poi.html.util.RenderUtils; import org.ddr.poi.util.XmlUtils; import org.jsoup.nodes.Element; import java.util.List; /** * 表格单元格标签渲染器 * * @author Draco * @since 2021-03-04 */ public class TableCellRenderer implements ElementRenderer { private static final String[] TAGS = {HtmlConstants.TAG_TH, HtmlConstants.TAG_TD}; /** * 开始渲染 * * @param element HTML元素 * @param context 渲染上下文 * @return 是否继续渲染子元素 */ @Override public boolean renderStart(Element element, HtmlRenderContext context) { CSSStyleDeclarationImpl styleDeclaration = context.currentElementStyle(); int row = NumberUtils.toInt(element.attr(HtmlConstants.ATTR_ROW_INDEX)); int column = NumberUtils.toInt(element.attr(HtmlConstants.ATTR_COLUMN_INDEX)); XWPFTable table = context.getClosestTable(); XWPFTableCell cell = table.getRow(row).getCell(column); context.pushContainer(cell); XWPFParagraph paragraph = cell.getParagraphArray(0); XmlCursor newCursor = paragraph.getCTP().newCursor(); // 指针指向单元格默认添加的段落,所有内容将被添加到该段落之前 context.pushCursor(newCursor); newCursor.dispose(); RenderUtils.cellStyle(context, cell, styleDeclaration); return true; } /** * 元素渲染结束需要执行的逻辑 * * @param element HTML元素 * @param context 渲染上下文 */ @Override public void renderEnd(Element element, HtmlRenderContext context) { List paragraphs = context.getContainer().getParagraphs(); if (paragraphs.size() > 1) { XmlCursor xmlCursor = context.currentCursorObject().newCursor(); if (xmlCursor.toPrevSibling() && XmlUtils.P_QNAME.equals(xmlCursor.getName())) { ((XWPFTableCell) context.getContainer()).removeParagraph(paragraphs.size() - 1); } xmlCursor.dispose(); } context.popContainer(); context.popCursor(); context.unmarkDedupe(); } @Override public String[] supportedTags() { return TAGS; } @Override public boolean renderAsBlock() { // 本身仅作为容器 return false; } } ================================================ FILE: src/main/java/org/ddr/poi/html/tag/TableRenderer.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.html.tag; import com.steadystate.css.dom.CSSStyleDeclarationImpl; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.math.NumberUtils; import org.apache.poi.util.Units; import org.apache.poi.xwpf.usermodel.XWPFParagraph; import org.apache.poi.xwpf.usermodel.XWPFTable; import org.apache.poi.xwpf.usermodel.XWPFTableCell; import org.apache.poi.xwpf.usermodel.XWPFTableRow; import org.apache.xmlbeans.XmlCursor; import org.ddr.poi.html.ElementRenderer; import org.ddr.poi.html.HtmlConstants; import org.ddr.poi.html.HtmlRenderContext; import org.ddr.poi.html.util.CSSLength; import org.ddr.poi.html.util.CSSLengthUnit; import org.ddr.poi.html.util.CSSStyleUtils; import org.ddr.poi.html.util.ColumnStyle; import org.ddr.poi.html.util.JsoupUtils; import org.ddr.poi.html.util.RenderUtils; import org.ddr.poi.html.util.Span; import org.ddr.poi.html.util.SpanWidth; import org.ddr.poi.html.util.WhiteSpaceRule; import org.jsoup.nodes.Element; import org.jsoup.nodes.Node; import org.jsoup.select.Elements; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBorder; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTRow; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTbl; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblGrid; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblGridCol; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblWidth; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTc; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcPr; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTVMerge; import org.openxmlformats.schemas.wordprocessingml.x2006.main.STBorder; import org.openxmlformats.schemas.wordprocessingml.x2006.main.STMerge; import org.openxmlformats.schemas.wordprocessingml.x2006.main.STTblWidth; import java.math.BigInteger; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.TreeMap; /** * 表格渲染器 * * @author Draco * @since 2021-02-18 */ public class TableRenderer implements ElementRenderer { private static final String[] TAGS = {HtmlConstants.TAG_TABLE}; private final CSSStyleDeclarationImpl defaultCaptionStyle = new CSSStyleDeclarationImpl(); public TableRenderer() { defaultCaptionStyle.setBackgroundColor(""); defaultCaptionStyle.setBorder(""); defaultCaptionStyle.setPadding(""); defaultCaptionStyle.setMargin(""); defaultCaptionStyle.setTextAlign(HtmlConstants.CENTER); CSSStyleUtils.split(defaultCaptionStyle); } /** * 开始渲染 * * @param element HTML元素 * @param context 渲染上下文 * @return 是否继续渲染子元素 */ @Override public boolean renderStart(Element element, HtmlRenderContext context) { CSSStyleDeclarationImpl styleDeclaration = context.currentElementStyle(); WhiteSpaceRule tableWhiteSpace = WhiteSpaceRule.of(styleDeclaration.getWhiteSpace(), WhiteSpaceRule.NORMAL); styleDeclaration.setWhiteSpace(HtmlConstants.NORMAL); Map whiteSpaceMap = new HashMap<>(); whiteSpaceMap.put(element, tableWhiteSpace); String widthDeclaration = styleDeclaration.getWidth(); XWPFTable table = context.getClosestTable(); int containerWidth = context.getAvailableWidthInEMU(); CSSLength width = CSSLength.of(widthDeclaration); boolean explicitWidth = width.isValid() && !width.isPercent(); int tableWidth = context.computeLengthInEMU(widthDeclaration, styleDeclaration.getMaxWidth(), containerWidth, containerWidth); int originWidth = !width.isValid() || width.isPercent() ? tableWidth : context.lengthToEMU(width); reorderTableChildren(element); Element caption = JsoupUtils.firstChild(element, HtmlConstants.TAG_CAPTION); if (caption != null) { renderCaption(context, table, caption, tableWhiteSpace); } Element colgroup = JsoupUtils.firstChild(element, HtmlConstants.TAG_COLGROUP); List columnStyles = extractColumnStyles(colgroup); CTTbl ctTbl = table.getCTTbl(); handleTableProperties(element, ctTbl); boolean groupsRules = HtmlConstants.GROUPS.equals(element.attr(HtmlConstants.ATTR_RULES)); Elements trs = JsoupUtils.childRows(element); Map rowSpanMap = new TreeMap<>(); TreeMap colWidthMap = new TreeMap<>(); LinkedHashSet spanWidths = new LinkedHashSet<>(); Element lastTrParent = null; for (int r = 0; r < trs.size(); r++) { Element tr = trs.get(r); Element trParent = tr.parent(); boolean renderGroupsGap = false; if (lastTrParent != trParent) { renderGroupsGap = groupsRules && lastTrParent!= null; lastTrParent = trParent; } XWPFTableRow row = createRow(table, r); Elements tds = JsoupUtils.children(tr, HtmlConstants.TAG_TH, HtmlConstants.TAG_TD); int columnIndex = 0; int minRowSpan = 1; int vMergeCount = 0; for (int c = 0; c < tds.size(); c++) { Element td = tds.get(c); int rowspan = NumberUtils.toInt(td.attr(HtmlConstants.ATTR_ROWSPAN), 1); int colspan = NumberUtils.toInt(td.attr(HtmlConstants.ATTR_COLSPAN), 1); minRowSpan = Math.min(minRowSpan, rowspan); for (Map.Entry entry : rowSpanMap.entrySet()) { if (entry.getKey() <= columnIndex && entry.getValue().isEnabled()) { columnIndex += entry.getValue().getColumn(); entry.getValue().setEnabled(false); // 合并行也需要生成单元格 addVMergeCell(context, row, c, entry.getValue()); vMergeCount++; } } // 标记行列索引,便于渲染单元格时获取容器 td.attr(HtmlConstants.ATTR_ROW_INDEX, String.valueOf(r)); td.attr(HtmlConstants.ATTR_COLUMN_INDEX, String.valueOf(c + vMergeCount)); WhiteSpaceRule tdWhiteSpace = null; Element parent = td.parent(); while (true) { if (parent == element) { if (tdWhiteSpace == null) { tdWhiteSpace = tableWhiteSpace; } break; } WhiteSpaceRule parentWhiteSpace = whiteSpaceMap.get(parent); if (!whiteSpaceMap.containsKey(parent)) { CSSStyleDeclarationImpl parentStyle = context.getCssStyleDeclaration(parent); String parentRule = parentStyle.removeProperty(HtmlConstants.CSS_WHITE_SPACE); parentWhiteSpace = WhiteSpaceRule.of(parentRule); whiteSpaceMap.put(parent, parentWhiteSpace); if (parentWhiteSpace != null) { parent.attr(HtmlConstants.ATTR_STYLE, parentStyle.getCssText()); } } if (parentWhiteSpace != null && tdWhiteSpace == null) { tdWhiteSpace = parentWhiteSpace; } parent = parent.parent(); } if (!tdWhiteSpace.isNormal()) { td.attr(HtmlConstants.ATTR_STYLE, HtmlConstants.CSS_WHITE_SPACE + HtmlConstants.COLON + tdWhiteSpace.getValue() + HtmlConstants.SEMICOLON + td.attr(HtmlConstants.ATTR_STYLE)); } // 列定义的样式与单元格的样式合并 if (!columnStyles.isEmpty() && columnIndex < columnStyles.size()) { String colStyle = columnStyles.get(columnIndex).getStyle().getCssText(); StringBuilder sb = new StringBuilder(); if (!colStyle.isEmpty()) { sb.append(colStyle).append(HtmlConstants.SEMICOLON); } if (colspan > 1) { CSSLength tdWidth = sumColumnWidths(columnStyles, columnIndex, colspan); if (tdWidth.isValid()) { sb.append(HtmlConstants.CSS_WIDTH).append(HtmlConstants.COLON) .append(tdWidth).append(HtmlConstants.SEMICOLON); } } if (sb.length() > 0) { sb.append(td.attr(HtmlConstants.ATTR_STYLE)); td.attr(HtmlConstants.ATTR_STYLE, sb.toString()); } } CSSStyleDeclarationImpl tdStyleDeclaration = CSSStyleUtils.parse(td.attr(HtmlConstants.ATTR_STYLE)); CSSLength tdWidth = CSSLength.of(tdStyleDeclaration.getWidth()); // 必须晚于之前列的行合并单元格创建 XWPFTableCell cell = createCell(row, c); CTTcPr ctTcPr = RenderUtils.getTcPr(cell.getCTTc()); if (rowspan > 1) { CSSStyleUtils.split(tdStyleDeclaration); rowSpanMap.put(columnIndex, new Span(rowspan, colspan, false, tdStyleDeclaration)); CTVMerge ctvMerge = ctTcPr.isSetVMerge() ? ctTcPr.getVMerge() : ctTcPr.addNewVMerge(); ctvMerge.setVal(STMerge.RESTART); } if (colspan == 1) { CSSLength existingWidth = colWidthMap.get(columnIndex); if (existingWidth == null || !existingWidth.isValid()) { colWidthMap.put(columnIndex, tdWidth); } else { // 根据表格本身是否使用百分比的宽度定义,来确定单元格宽度的定义方式 if (explicitWidth) { if (existingWidth.isPercent()) { colWidthMap.put(columnIndex, tdWidth); } } else { if (!existingWidth.isPercent()) { colWidthMap.put(columnIndex, tdWidth); } } } } else { spanWidths.add(new SpanWidth(tdWidth, columnIndex, colspan, explicitWidth)); ctTcPr.addNewGridSpan().setVal(BigInteger.valueOf(colspan)); } if (renderGroupsGap) { RenderUtils.getTableCellTop(cell.getCTTc()).setVal(STBorder.SINGLE); } columnIndex += colspan; } for (Iterator> iterator = rowSpanMap.entrySet().iterator(); iterator.hasNext(); ) { Map.Entry entry = iterator.next(); Integer spanColumnIndex = entry.getKey(); Span span = entry.getValue(); span.setRow(span.getRow() - minRowSpan); if (span.getRow() == 0) { iterator.remove(); } else { span.setEnabled(true); } if (columnIndex <= spanColumnIndex && columnIndex < colWidthMap.size()) { addVMergeCell(context, row, columnIndex, span); columnIndex += span.getColumn(); } } } CTTblGrid tblGrid = ctTbl.getTblGrid(); if (tblGrid == null) { tblGrid = ctTbl.addNewTblGrid(); } for (SpanWidth spanWidth : spanWidths) { spanWidth.setLength(colWidthMap); } BigInteger[] colWidths = new BigInteger[colWidthMap.size()]; // 未处理的百分比总和 double unhandledPercentSum = 0; // 未处理的emu长度总和 int unhandledEmuSum = 0; // 未处理的emu长度数量 int unhandledEmuCount = 0; // 可用的宽度 int remainWidth = tableWidth; for (Iterator> iterator = colWidthMap.entrySet().iterator(); iterator.hasNext(); ) { Map.Entry entry = iterator.next(); CSSLength value = entry.getValue(); if (!value.isValid()) { entry.setValue(new CSSLength(100d / colWidths.length, CSSLengthUnit.PERCENT)); unhandledPercentSum += entry.getValue().getValue(); continue; } if (explicitWidth) { if (value.isPercent()) { unhandledPercentSum += value.getValue(); } else { int emu = (int) ((long) value.toEMU() * tableWidth / originWidth); colWidths[entry.getKey()] = BigInteger.valueOf(RenderUtils.emuToTwips(emu)); remainWidth -= emu; iterator.remove(); } } else { if (value.isPercent()) { unhandledPercentSum += value.getValue(); } else { unhandledPercentSum += 100d / colWidths.length; unhandledEmuSum += value.toEMU(); unhandledEmuCount++; } } } // 处理剩余未处理的列宽 for (Map.Entry entry : colWidthMap.entrySet()) { CSSLength value = entry.getValue(); if (value.isPercent()) { colWidths[entry.getKey()] = BigInteger.valueOf((int) Math.rint(remainWidth * value.getValue() / unhandledPercentSum * 20 / Units.EMU_PER_POINT)); } else { colWidths[entry.getKey()] = BigInteger.valueOf((int) Math.rint(remainWidth * (unhandledEmuCount * 100d / colWidths.length / unhandledPercentSum) * value.toEMU() / unhandledEmuSum * 20 / Units.EMU_PER_POINT)); } } for (BigInteger colWidth : colWidths) { CTTblGridCol ctTblGridCol = tblGrid.addNewGridCol(); ctTblGridCol.setW(colWidth); } for (int i = 0, rows = ctTbl.sizeOfTrArray(); i < rows; i++) { CTRow ctRow = ctTbl.getTrArray(i); int columnIndex = 0; for (int j = 0, cells = ctRow.sizeOfTcArray(); j < cells; j++) { CTTc ctTc = ctRow.getTcArray(j); CTTcPr tcPr = RenderUtils.getTcPr(ctTc); int colspan = tcPr.isSetGridSpan() ? tcPr.getGridSpan().getVal().intValue() : 1; CTTblWidth tcWidth = tcPr.addNewTcW(); tcWidth.setType(STTblWidth.DXA); if (colspan == 1) { tcWidth.setW(colWidths[columnIndex]); } else { int sum = 0; for (int k = 0; k < colspan; k++) { sum += colWidths[columnIndex + k].intValue(); } tcWidth.setW(BigInteger.valueOf(sum)); } columnIndex += colspan; } } for (Element tr : trs) { for (Element td : tr.children()) { if (!td.hasAttr(HtmlConstants.ATTR_COLUMN_INDEX)) { continue; } int colspan = NumberUtils.toInt(td.attr(HtmlConstants.ATTR_COLSPAN), 1); int columnIndex = Integer.parseInt(td.attr(HtmlConstants.ATTR_COLUMN_INDEX)); int sum = 0; if (colspan == 1) { sum = colWidths[columnIndex].intValue(); } else { for (int k = 0; k < colspan; k++) { sum += colWidths[columnIndex + k].intValue(); } } String tdWidth = sum + CSSLengthUnit.TWIP.getLiteral(); String style = td.attr(HtmlConstants.ATTR_STYLE).trim(); if (style.isEmpty()) { style = HtmlConstants.ATTR_WIDTH + HtmlConstants.COLON + tdWidth; } else { style += (style.endsWith(HtmlConstants.SEMICOLON) ? "" : HtmlConstants.SEMICOLON) + HtmlConstants.ATTR_WIDTH + HtmlConstants.COLON + tdWidth; } td.attr(HtmlConstants.ATTR_STYLE, style); } } return true; } private void reorderTableChildren(Element element) { Elements theads = JsoupUtils.children(element, HtmlConstants.TAG_THEAD); for (int i = theads.size() - 1; i >= 0; i--) { Element thead = theads.get(i); thead.remove(); element.prependChild(thead); } Elements tfoots = JsoupUtils.children(element, HtmlConstants.TAG_TFOOT); for (Element tfoot : tfoots) { tfoot.remove(); element.appendChild(tfoot); } } /** * link */ private void handleTableProperties(Element element, CTTbl ctTbl) { String frameValue = null; String rulesValue = null; int borderValue = 0; if (element.hasAttr(HtmlConstants.BORDER)) { String border = element.attr(HtmlConstants.BORDER); frameValue = HtmlConstants.VOID; rulesValue = HtmlConstants.NONE; if (!"0".equals(border)) { if (StringUtils.isNumeric(border) && !border.startsWith("0") && (borderValue = Integer.parseInt(border)) > 0) { frameValue = ""; } else { frameValue = HtmlConstants.BORDER; } rulesValue = HtmlConstants.ALL; } } if (element.hasAttr(HtmlConstants.ATTR_FRAME)) { frameValue = element.attr(HtmlConstants.ATTR_FRAME); } if (element.hasAttr(HtmlConstants.ATTR_RULES)) { rulesValue = element.attr(HtmlConstants.ATTR_RULES); } if (frameValue != null) { CTBorder tableTop = RenderUtils.getTableTop(ctTbl); CTBorder tableBottom = RenderUtils.getTableBottom(ctTbl); CTBorder tableLeft = RenderUtils.getTableLeft(ctTbl); CTBorder tableRight = RenderUtils.getTableRight(ctTbl); switch (frameValue) { case HtmlConstants.VOID: tableTop.setVal(STBorder.NONE); tableBottom.setVal(STBorder.NONE); tableLeft.setVal(STBorder.NONE); tableRight.setVal(STBorder.NONE); break; case HtmlConstants.ABOVE: tableTop.setVal(STBorder.SINGLE); tableBottom.setVal(STBorder.NONE); tableLeft.setVal(STBorder.NONE); tableRight.setVal(STBorder.NONE); break; case HtmlConstants.BELOW: tableTop.setVal(STBorder.NONE); tableBottom.setVal(STBorder.SINGLE); tableLeft.setVal(STBorder.NONE); tableRight.setVal(STBorder.NONE); break; case HtmlConstants.LHS: tableTop.setVal(STBorder.NONE); tableBottom.setVal(STBorder.NONE); tableLeft.setVal(STBorder.SINGLE); tableRight.setVal(STBorder.NONE); break; case HtmlConstants.RHS: tableTop.setVal(STBorder.NONE); tableBottom.setVal(STBorder.NONE); tableLeft.setVal(STBorder.NONE); tableRight.setVal(STBorder.SINGLE); break; case HtmlConstants.H_SIDES: tableTop.setVal(STBorder.SINGLE); tableBottom.setVal(STBorder.SINGLE); tableLeft.setVal(STBorder.NONE); tableRight.setVal(STBorder.NONE); break; case HtmlConstants.V_SIDES: tableTop.setVal(STBorder.NONE); tableBottom.setVal(STBorder.NONE); tableLeft.setVal(STBorder.SINGLE); tableRight.setVal(STBorder.SINGLE); break; case HtmlConstants.BOX: case HtmlConstants.BORDER: tableTop.setVal(STBorder.SINGLE); tableBottom.setVal(STBorder.SINGLE); tableLeft.setVal(STBorder.SINGLE); tableRight.setVal(STBorder.SINGLE); break; default: tableTop.setVal(STBorder.SINGLE); tableBottom.setVal(STBorder.SINGLE); tableLeft.setVal(STBorder.SINGLE); tableRight.setVal(STBorder.SINGLE); if (borderValue > 0) { long thickness = (long) RenderUtils.BORDER_WIDTH_PER_PX * borderValue; thickness = Math.min(thickness, RenderUtils.MAX_BORDER_WIDTH); BigInteger sz = BigInteger.valueOf(thickness); tableTop.setSz(sz); tableBottom.setSz(sz); tableLeft.setSz(sz); tableRight.setSz(sz); } break; } } if (rulesValue != null) { switch (rulesValue) { case HtmlConstants.NONE: case HtmlConstants.GROUPS: RenderUtils.getTblInsideH(RenderUtils.getTblBorders(ctTbl)).setVal(STBorder.NONE); RenderUtils.getTblInsideV(RenderUtils.getTblBorders(ctTbl)).setVal(STBorder.NONE); break; case HtmlConstants.ROWS: RenderUtils.getTblInsideH(RenderUtils.getTblBorders(ctTbl)).setVal(STBorder.SINGLE); RenderUtils.getTblInsideV(RenderUtils.getTblBorders(ctTbl)).setVal(STBorder.NONE); break; case HtmlConstants.COLS: RenderUtils.getTblInsideH(RenderUtils.getTblBorders(ctTbl)).setVal(STBorder.NONE); RenderUtils.getTblInsideV(RenderUtils.getTblBorders(ctTbl)).setVal(STBorder.SINGLE); break; case HtmlConstants.ALL: default: RenderUtils.getTblInsideH(RenderUtils.getTblBorders(ctTbl)).setVal(STBorder.SINGLE); RenderUtils.getTblInsideV(RenderUtils.getTblBorders(ctTbl)).setVal(STBorder.SINGLE); break; } } } private List extractColumnStyles(Element colgroup) { List columnStyles = Collections.emptyList(); if (colgroup != null) { Elements cols = colgroup.select(HtmlConstants.TAG_COL); columnStyles = new ArrayList<>(); for (Element col : cols) { String style = col.attr(HtmlConstants.ATTR_STYLE); CSSStyleDeclarationImpl cssStyleDeclaration = CSSStyleUtils.parse(style); int span = NumberUtils.toInt(col.attr(HtmlConstants.ATTR_SPAN), 1); // 宽度样式优先于宽度属性 CSSLength colWidth = CSSLength.of(cssStyleDeclaration.getWidth()); if (!colWidth.isValid()) { String colWidthAttr = col.attr(HtmlConstants.ATTR_WIDTH); if (!colWidthAttr.isEmpty()) { if (!colWidthAttr.endsWith(HtmlConstants.PERCENT)) { colWidthAttr += HtmlConstants.PX; } colWidth = CSSLength.of(colWidthAttr); } } if (colWidth.isValid() && span > 1) { colWidth = new CSSLength(colWidth.getValue() / span, colWidth.getUnit()); } for (int i = 0; i < span; i++) { columnStyles.add(new ColumnStyle(cssStyleDeclaration, colWidth)); } } colgroup.remove(); } return columnStyles; } private CSSLength sumColumnWidths(List columnWidths, int columnIndex, int colspan) { Boolean percent = null; double sum = 0d; int availableSpan = Math.min(columnWidths.size() - columnIndex, colspan); for (int i = 0; i < availableSpan; i++) { CSSLength width = columnWidths.get(columnIndex + i).getWidth(); if (!width.isValid()) { return width; } if (percent == null) { percent = width.isPercent(); } else if (percent ^ width.isPercent()) { return CSSLength.INVALID; } if (percent) { sum += width.getValue(); } else { sum += width.toEMU(); } } return new CSSLength(sum, percent ? CSSLengthUnit.PERCENT : CSSLengthUnit.EMU); } /** * 渲染标题 * * @param context 渲染上下文 * @param table 表格 * @param caption 标题元素 * @param whiteSpace table标签上的white-space样式 */ private void renderCaption(HtmlRenderContext context, XWPFTable table, Element caption, WhiteSpaceRule whiteSpace) { CSSStyleDeclarationImpl captionStyle = context.getCssStyleDeclaration(caption); captionStyle.getProperties().addAll(0, defaultCaptionStyle.getProperties()); if (whiteSpace != null) { captionStyle.getProperties().add(0, CSSStyleUtils.newProperty(HtmlConstants.CSS_WHITE_SPACE, whiteSpace.getValue())); } context.pushInlineStyle(captionStyle, caption.isBlock()); XWPFParagraph captionParagraph; boolean bottom = HtmlConstants.BOTTOM.equals(context.getPropertyValue(HtmlConstants.CSS_CAPTION_SIDE)); if (bottom) { captionParagraph = context.getClosestParagraph(); caption.parent().attr(HtmlConstants.CSS_CAPTION_SIDE, HtmlConstants.BOTTOM); } else { // 表格上方添加标题 XmlCursor xmlCursor = table.getCTTbl().newCursor(); context.pushCursor(xmlCursor); captionParagraph = context.newParagraph(null, xmlCursor); xmlCursor.dispose(); } RenderUtils.paragraphStyle(context, captionParagraph, captionStyle); context.markDedupe(captionParagraph); for (Node node : caption.childNodes()) { context.renderNode(node); } context.unmarkDedupe(); context.popInlineStyle(); caption.remove(); if (bottom) { XmlCursor xmlCursor = captionParagraph.getCTP().newCursor(); context.pushCursor(xmlCursor); xmlCursor.dispose(); } else { context.popCursor(); } } /** * 元素渲染结束需要执行的逻辑 * * @param element HTML元素 * @param context 渲染上下文 */ @Override public void renderEnd(Element element, HtmlRenderContext context) { if (HtmlConstants.BOTTOM.equals(element.attr(HtmlConstants.CSS_CAPTION_SIDE))) { context.popCursor(); } } /** * 添加垂直合并的单元格 * * @param row 行 * @param columnIndex 列索引 * @param span 跨行列参数 */ private void addVMergeCell(HtmlRenderContext context, XWPFTableRow row, int columnIndex, Span span) { XWPFTableCell cell = createCell(row, columnIndex); CTTcPr ctTcPr = RenderUtils.getTcPr(cell.getCTTc()); ctTcPr.addNewVMerge(); RenderUtils.setBorder(context, cell, span.getStyle()); if (span.getColumn() > 1) { ctTcPr.addNewGridSpan().setVal(BigInteger.valueOf(span.getColumn())); } } /** * 创建单元格 * * @param row 行 * @param c 列索引 * @return 单元格 */ private XWPFTableCell createCell(XWPFTableRow row, int c) { return row.createCell(); } /** * 创建行 * * @param table 表格 * @param r 行索引 * @return 行 */ private XWPFTableRow createRow(XWPFTable table, int r) { // 避免使用createRow,因为不需要自动创建单元格 return table.insertNewTableRow(r); } @Override public String[] supportedTags() { return TAGS; } @Override public boolean renderAsBlock() { return true; } } ================================================ FILE: src/main/java/org/ddr/poi/html/tag/UnderlineRenderer.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.html.tag; import org.ddr.poi.html.ElementRenderer; import org.ddr.poi.html.HtmlConstants; import org.ddr.poi.html.HtmlRenderContext; import org.ddr.poi.html.util.CSSStyleUtils; import org.jsoup.nodes.Element; /** * u标签渲染器 * * @author Draco * @since 2021-02-23 */ public class UnderlineRenderer implements ElementRenderer { private static final String[] TAGS = {HtmlConstants.TAG_U}; /** * 开始渲染 * * @param element HTML元素 * @param context 渲染上下文 * @return 是否继续渲染子元素 */ @Override public boolean renderStart(Element element, HtmlRenderContext context) { context.pushInlineStyle(CSSStyleUtils.parse(HtmlConstants.DEFINED_UNDERLINE), element.isBlock()); return true; } /** * 元素渲染结束需要执行的逻辑 * * @param element HTML元素 * @param context 渲染上下文 */ @Override public void renderEnd(Element element, HtmlRenderContext context) { context.popInlineStyle(); } @Override public String[] supportedTags() { return TAGS; } @Override public boolean renderAsBlock() { return false; } } ================================================ FILE: src/main/java/org/ddr/poi/html/tag/WalkThroughRenderer.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.html.tag; import org.ddr.poi.html.ElementRenderer; import org.ddr.poi.html.HtmlConstants; import org.ddr.poi.html.HtmlRenderContext; import org.jsoup.nodes.Element; /** * 穿透的元素,即标签仅作为样式的载体,但是不渲染为内容容器 * * @author Draco * @since 2021-03-15 */ public class WalkThroughRenderer implements ElementRenderer { private static final String[] TAGS = { HtmlConstants.TAG_HTML, HtmlConstants.TAG_BODY, HtmlConstants.TAG_THEAD, HtmlConstants.TAG_TBODY, HtmlConstants.TAG_TR, HtmlConstants.TAG_TFOOT }; /** * 开始渲染 * * @param element HTML元素 * @param context 渲染上下文 * @return 是否继续渲染子元素 */ @Override public boolean renderStart(Element element, HtmlRenderContext context) { return true; } @Override public String[] supportedTags() { return TAGS; } @Override public boolean renderAsBlock() { return false; } } ================================================ FILE: src/main/java/org/ddr/poi/html/util/BoxProperty.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.html.util; import com.steadystate.css.dom.CSSStyleDeclarationImpl; import com.steadystate.css.dom.Property; import org.ddr.poi.html.HtmlConstants; import org.w3c.dom.css.CSSValue; /** * CSS中支持四个边的属性 * * @author Draco * @since 2021-03-29 */ public enum BoxProperty { MARGIN(HtmlConstants.CSS_MARGIN_TOP, HtmlConstants.CSS_MARGIN_RIGHT, HtmlConstants.CSS_MARGIN_BOTTOM, HtmlConstants.CSS_MARGIN_LEFT), PADDING(HtmlConstants.CSS_PADDING_TOP, HtmlConstants.CSS_PADDING_RIGHT, HtmlConstants.CSS_PADDING_BOTTOM, HtmlConstants.CSS_PADDING_LEFT), BORDER_STYLE(HtmlConstants.CSS_BORDER_TOP_STYLE, HtmlConstants.CSS_BORDER_RIGHT_STYLE, HtmlConstants.CSS_BORDER_BOTTOM_STYLE, HtmlConstants.CSS_BORDER_LEFT_STYLE), BORDER_WIDTH(HtmlConstants.CSS_BORDER_TOP_WIDTH, HtmlConstants.CSS_BORDER_RIGHT_WIDTH, HtmlConstants.CSS_BORDER_BOTTOM_WIDTH, HtmlConstants.CSS_BORDER_LEFT_WIDTH), BORDER_COLOR(HtmlConstants.CSS_BORDER_TOP_COLOR, HtmlConstants.CSS_BORDER_RIGHT_COLOR, HtmlConstants.CSS_BORDER_BOTTOM_COLOR, HtmlConstants.CSS_BORDER_LEFT_COLOR); private final String top; private final String right; private final String bottom; private final String left; BoxProperty(String top, String right, String bottom, String left) { this.top = top; this.right = right; this.bottom = bottom; this.left = left; } public void setValues(CSSStyleDeclarationImpl cssStyleDeclaration, int i, CSSValue topValue, CSSValue rightValue, CSSValue bottomValue, CSSValue leftValue) { cssStyleDeclaration.getProperties().add(i, new Property(top, topValue, false)); cssStyleDeclaration.getProperties().add(i, new Property(right, rightValue, false)); cssStyleDeclaration.getProperties().add(i, new Property(bottom, bottomValue, false)); cssStyleDeclaration.getProperties().add(i, new Property(left, leftValue, false)); } public void setValues(CSSStyleDeclarationImpl cssStyleDeclaration, int i, CSSValue value) { setValues(cssStyleDeclaration, i, value, value, value, value); } public void setValues(CSSStyleDeclarationImpl cssStyleDeclaration, int i, CSSValue topBottom, CSSValue rightLeft) { setValues(cssStyleDeclaration, i, topBottom, rightLeft, topBottom, rightLeft); } public void setValues(CSSStyleDeclarationImpl cssStyleDeclaration, int i, CSSValue top, CSSValue rightLeft, CSSValue bottom) { setValues(cssStyleDeclaration, i, top, rightLeft, bottom, rightLeft); } } ================================================ FILE: src/main/java/org/ddr/poi/html/util/CSSLength.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.html.util; import java.util.Arrays; import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; /** * CSS长度值 * * @author Draco * @since 2021-03-01 */ public class CSSLength { private static final Pattern LENGTH_PATTERN = Pattern.compile( "(-?\\d+\\.?\\d*)(" + Arrays.stream(CSSLengthUnit.values()).filter(CSSLengthUnit::isSystem) .map(CSSLengthUnit::getLiteral).collect(Collectors.joining("|")) + ")" ); public static final CSSLength INVALID = new CSSLength(Double.NaN, null); private double value; private CSSLengthUnit unit; public CSSLength(double value, CSSLengthUnit unit) { this.value = value; this.unit = unit; } public double getValue() { return value; } public CSSLengthUnit getUnit() { return unit; } @Override public String toString() { return String.format("%.2f", value) + (unit == null ? "" : unit.getLiteral()); } /** * 单位转EMU,适用于绝对长度单位 */ public int toEMU() { validate(); requireAbsoluteUnit(); return (int) Math.rint(unitValue()); } public double unitValue() { return value * unit.absoluteFactor(); } private void requireAbsoluteUnit() { if (unit.isRelative()) { throw new UnsupportedOperationException("Can not convert a relative length to EMU: " + toString()); } } private void validate() { if (!isValid()) { throw new UnsupportedOperationException("Invalid CSS length"); } } public CSSLength to(CSSLengthUnit other) { validate(); return new CSSLength(value * unit.to(other), other); } public int toHalfPoints() { validate(); return (int) Math.rint(value * unit.to(CSSLengthUnit.PT) * 2); } public boolean isValid() { return unit != null && !Double.isNaN(value) && !Double.isInfinite(value); } public boolean isPercent() { return unit == CSSLengthUnit.PERCENT; } public boolean isValidPercent() { return isValid() && isPercent(); } public static CSSLength of(String text) { if (text == null || text.isEmpty()) { return INVALID; } Matcher matcher = LENGTH_PATTERN.matcher(text); if (matcher.matches()) { return new CSSLength(Double.parseDouble(matcher.group(1)), CSSLengthUnit.of(matcher.group(2))); } return INVALID; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; CSSLength other = (CSSLength) o; if (!isValid() && !other.isValid()) return true; return Double.compare(other.value, value) == 0 && unit == other.unit; } @Override public int hashCode() { return Objects.hash(value, unit); } } ================================================ FILE: src/main/java/org/ddr/poi/html/util/CSSLengthUnit.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.html.util; import org.apache.poi.util.Units; import org.ddr.poi.html.HtmlConstants; import java.util.Arrays; import java.util.Map; import java.util.Objects; import java.util.function.Function; import java.util.stream.Collectors; /** * 长度单位 * * @author Draco * @since 2021-03-01 */ public enum CSSLengthUnit { CM(HtmlConstants.CM, true, false, false, Units.EMU_PER_CENTIMETER, 1, 1), MM(HtmlConstants.MM, true, false, false, Units.EMU_PER_CENTIMETER, 10, -1), IN(HtmlConstants.IN, true, false, false, Units.EMU_PER_POINT, 72, 1), PX(HtmlConstants.PX, true, false, false, Units.EMU_PER_PIXEL, 1, 1), PT(HtmlConstants.PT, true, false, false, Units.EMU_PER_POINT, 1, 1), PC(HtmlConstants.PC, true, false, false, Units.EMU_PER_POINT, 12, 1), EMU(HtmlConstants.EMU, false, false, false, 1, 1, 1), TWIP(HtmlConstants.TWIP, false, false, false, Units.EMU_PER_POINT, 20, -1), REM(HtmlConstants.REM, true, true, false, 1, 1, 1), EM(HtmlConstants.EM, true, true, true, 1, 1, 1), VW(HtmlConstants.VW, true, true, false, 1, 100, -1), VH(HtmlConstants.VH, true, true, false, 1, 100, -1), VMIN(HtmlConstants.VMIN, true, true, false, 1, 100, -1), VMAX(HtmlConstants.VMAX, true, true, false, 1, 100, -1), PERCENT(HtmlConstants.PERCENT, true, true, true, 1, 100, -1); private static final Map LITERAL_MAP = Arrays.stream(values()) .filter(CSSLengthUnit::isSystem) .collect(Collectors.toMap(CSSLengthUnit::getLiteral, Function.identity())); /** * 单位字面值 */ private final String literal; /** * 是否为系统单位,该枚举中包含部分自定义单位以便换算 */ private final boolean system; /** * 是否为相对长度 */ private final boolean relative; /** * 是否相对父元素,false表示相对于根元素 */ private final boolean relativeToParent; // 下面3个属性联合表示单位系数,绝对长度以EMU为基准,相对长度以1为基准 private final int unit; private final int factor; private final int power; CSSLengthUnit(String literal, boolean system, boolean relative, boolean relativeToParent, int unit, int factor, int power) { this.literal = literal; this.system = system; this.relative = relative; this.relativeToParent = relativeToParent; this.unit = unit; this.factor = factor; this.power = power; } public String getLiteral() { return literal; } public boolean isSystem() { return system; } public boolean isRelative() { return relative; } public boolean isRelativeToParent() { return relativeToParent; } @Override public String toString() { return literal; } public double absoluteFactor() { return unit * Math.pow(factor, power); } public double to(CSSLengthUnit other) { Objects.requireNonNull(other, "Target CSS length unit must not be null"); if (this.isRelative()) { throw new IllegalArgumentException("Can not convert from a relative unit"); } if (other.isRelative()) { throw new IllegalArgumentException("Can not convert to a relative unit"); } return absoluteFactor() / other.absoluteFactor(); } /** * 单位字面值转为单位 * * @param literal 单位字面值 * @return 长度单位 */ public static CSSLengthUnit of(String literal) { return LITERAL_MAP.get(literal); } } ================================================ FILE: src/main/java/org/ddr/poi/html/util/CSSStyleUtils.java ================================================ package org.ddr.poi.html.util; import com.steadystate.css.dom.CSSStyleDeclarationImpl; import com.steadystate.css.dom.CSSValueImpl; import com.steadystate.css.dom.Property; import com.steadystate.css.parser.CSSOMParser; import com.steadystate.css.parser.SACParserCSS3; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.math.NumberUtils; import org.ddr.poi.html.HtmlConstants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.css.sac.InputSource; import org.w3c.dom.css.CSSValue; import java.io.IOException; import java.io.StringReader; /** * CSS样式相关的工具类 * * @author Draco * @since 2021-10-11 */ public class CSSStyleUtils { private static final Logger log = LoggerFactory.getLogger(CSSStyleUtils.class); /** * 空样式 */ public static final CSSStyleDeclarationImpl EMPTY_STYLE = new EmptyCSSStyle(); /** * 样式是否为空 * * @param style 样式声明 * @return 是否为空 */ public static boolean isEmpty(CSSStyleDeclarationImpl style) { return style == null || EMPTY_STYLE.equals(style) || style.getProperties().isEmpty(); } /** * CSS解析器 */ public static CSSOMParser newParser() { return new CSSOMParser(new SACParserCSS3()); } /** * 解析行内样式 * * @param inlineStyle 行内样式声明 * @return 样式 */ public static CSSStyleDeclarationImpl parse(String inlineStyle) { if (StringUtils.isBlank(inlineStyle)) { return new CSSStyleDeclarationImpl(); } try (StringReader sr = new StringReader(inlineStyle)) { return (CSSStyleDeclarationImpl) newParser().parseStyleDeclaration(new InputSource(sr)); } catch (IOException e) { log.warn("Inline style parse error: {}", inlineStyle, e); return EMPTY_STYLE; } } /** * 解析样式值 * * @param value 样式值字符串 * @return 样式值 */ public static CSSValue parseValue(String value) { try (StringReader sr = new StringReader(value)) { return newParser().parsePropertyValue(new InputSource(sr)); } catch (IOException e) { log.warn("CSS value parse error: {}", value, e); return new CSSValueImpl(); } } /** * 将样式键值对转换为样式属性 * * @param key 样式名称 * @param value 样式值 * @return 样式属性 */ public static Property newProperty(String key, String value) { return new Property(key, parseValue(value), false); } /** * 分解缩写的样式 * * @param style 样式声明 */ public static void split(CSSStyleDeclarationImpl style) { for (int i = style.getProperties().size() - 1; i >= 0; i--) { final Property p = style.getProperties().get(i); if (p != null && p.getValue() != null) { String name = p.getName().toLowerCase(); CSSValueImpl valueList = (CSSValueImpl) p.getValue(); int length = valueList.getLength(); // 将复合样式拆分成单属性样式 switch (name) { case HtmlConstants.CSS_BACKGROUND: splitBackground(valueList, length, style, i); break; case HtmlConstants.CSS_BORDER: splitBorder(valueList, length, style, i); break; case HtmlConstants.CSS_BORDER_TOP: splitBorder(valueList, length, style, i, HtmlConstants.CSS_BORDER_TOP_STYLE, HtmlConstants.CSS_BORDER_TOP_WIDTH, HtmlConstants.CSS_BORDER_TOP_COLOR); break; case HtmlConstants.CSS_BORDER_RIGHT: splitBorder(valueList, length, style, i, HtmlConstants.CSS_BORDER_RIGHT_STYLE, HtmlConstants.CSS_BORDER_RIGHT_WIDTH, HtmlConstants.CSS_BORDER_RIGHT_COLOR); break; case HtmlConstants.CSS_BORDER_BOTTOM: splitBorder(valueList, length, style, i, HtmlConstants.CSS_BORDER_BOTTOM_STYLE, HtmlConstants.CSS_BORDER_BOTTOM_WIDTH, HtmlConstants.CSS_BORDER_BOTTOM_COLOR); break; case HtmlConstants.CSS_BORDER_LEFT: splitBorder(valueList, length, style, i, HtmlConstants.CSS_BORDER_LEFT_STYLE, HtmlConstants.CSS_BORDER_LEFT_WIDTH, HtmlConstants.CSS_BORDER_LEFT_COLOR); break; case HtmlConstants.CSS_BORDER_STYLE: splitBox(valueList, length, style, i, BoxProperty.BORDER_STYLE); break; case HtmlConstants.CSS_BORDER_WIDTH: splitBox(valueList, length, style, i, BoxProperty.BORDER_WIDTH); break; case HtmlConstants.CSS_BORDER_COLOR: splitBox(valueList, length, style, i, BoxProperty.BORDER_COLOR); break; case HtmlConstants.CSS_FONT: splitFont(valueList, length, style, i); break; case HtmlConstants.CSS_MARGIN: splitBox(valueList, length, style, i, BoxProperty.MARGIN); break; case HtmlConstants.CSS_PADDING: splitBox(valueList, length, style, i, BoxProperty.PADDING); break; case HtmlConstants.CSS_LIST_STYLE: splitListStyle(valueList, length, style, i); break; case HtmlConstants.CSS_TEXT_DECORATION: splitTextDecoration(valueList, length, style, i); break; } } } } private static void splitBackground(CSSValueImpl valueList, int length, CSSStyleDeclarationImpl style, int i) { if (length == 0) { String cssText = valueList.getCssText().toLowerCase(); String color = Colors.fromStyle(cssText, null); if (color != null) { style.getProperties().add(i, new Property(HtmlConstants.CSS_BACKGROUND_COLOR, valueList, false)); } } else { for (int j = 0; j < length; j++) { CSSValue item = valueList.item(j); String cssText = item.getCssText().toLowerCase(); String color = Colors.fromStyle(cssText, null); if (color != null) { style.getProperties().add(i, new Property(HtmlConstants.CSS_BACKGROUND_COLOR, item, false)); break; } } } } private static void splitBorder(CSSValueImpl valueList, int length, CSSStyleDeclarationImpl style, int i) { if (length == 0) { String cssText = valueList.getCssText(); if (StringUtils.isNotBlank(cssText)) { handleBorderValue(style, i, valueList, cssText); } } else { for (int j = 0; j < length; j++) { CSSValue item = valueList.item(j); String value = item.getCssText(); handleBorderValue(style, i, item, value); } } } private static void splitBorder(CSSValueImpl valueList, int length, CSSStyleDeclarationImpl style, int i, String styleProperty, String widthProperty, String colorProperty) { if (length == 0) { String cssText = valueList.getCssText(); if (StringUtils.isNotBlank(cssText)) { handleBorderValue(style, i, valueList, cssText, styleProperty, widthProperty, colorProperty); } } else { for (int j = 0; j < length; j++) { CSSValue item = valueList.item(j); String value = item.getCssText(); handleBorderValue(style, i, item, value, styleProperty, widthProperty, colorProperty); } } } private static void handleBorderValue(CSSStyleDeclarationImpl style, int i, CSSValue item, String value) { value = value.toLowerCase(); if (HtmlConstants.BORDER_STYLES.contains(value)) { BoxProperty.BORDER_STYLE.setValues(style, i, item); } else if (NamedBorderWidth.contains(value)) { BoxProperty.BORDER_WIDTH.setValues(style, i, item); } else if (Character.isDigit(value.charAt(0))) { CSSLength width = CSSLength.of(value); if (width.isValid()) { BoxProperty.BORDER_WIDTH.setValues(style, i, item); } } else { BoxProperty.BORDER_COLOR.setValues(style, i, item); } } private static void handleBorderValue(CSSStyleDeclarationImpl style, int i, CSSValue item, String value, String styleProperty, String widthProperty, String colorProperty) { value = value.toLowerCase(); if (HtmlConstants.BORDER_STYLES.contains(value)) { style.getProperties().add(i, new Property(styleProperty, item, false)); } else if (NamedBorderWidth.contains(value)) { style.getProperties().add(i, new Property(widthProperty, item, false)); } else if (Character.isDigit(value.charAt(0))) { CSSLength width = CSSLength.of(value); if (width.isValid()) { style.getProperties().add(i, new Property(widthProperty, item, false)); } } else { style.getProperties().add(i, new Property(colorProperty, item, false)); } } private static void splitFont(CSSValueImpl valueList, int length, CSSStyleDeclarationImpl style, int i) { if (length == 0) { return; } boolean styleHandled = false; boolean sizeHandled = false; for (int j = 0; j < length; j++) { CSSValue item = valueList.item(j); String value = item.getCssText(); String lowerCase = value.toLowerCase(); if (!styleHandled && HtmlConstants.FONT_STYLES.contains(lowerCase)) { style.getProperties().add(i, new Property(HtmlConstants.CSS_FONT_STYLE, item, false)); styleHandled = true; } else if (HtmlConstants.FONT_VARIANTS.contains(lowerCase)) { style.getProperties().add(i, new Property(HtmlConstants.CSS_FONT_VARIANT_CAPS, item, false)); } else if (HtmlConstants.FONT_WEIGHTS.contains(lowerCase) || NumberUtils.isParsable(value)) { style.getProperties().add(i, new Property(HtmlConstants.CSS_FONT_WEIGHT, item, false)); } else if (HtmlConstants.SLASH.equals(value)) { // 字号与行高分隔符 // https://www.w3.org/TR/CSS22/fonts.html#value-def-absolute-size // xx-small, x-small, small, medium, large, x-large, xx-large, xxx-large // 1, , 2, 3, 4, 5, 6, 7 // FIXME font元素由于已废弃暂不支持 // 长度/百分比 CSSValue fontSize = valueList.item(j - 1); style.getProperties().add(i, new Property(HtmlConstants.CSS_FONT_SIZE, fontSize, false)); sizeHandled = true; if (++j < length) { // 数字/长度/百分比 CSSValue lineHeight = valueList.item(j); style.getProperties().add(i, new Property(HtmlConstants.CSS_LINE_HEIGHT, lineHeight, false)); } } else if (HtmlConstants.COMMA.equals(value)) { // 多个字体之间的分隔符 CSSValue firstFont = valueList.item(j - 1); if (!sizeHandled) { CSSValue fontSize = valueList.item(j - 2); style.getProperties().add(i, new Property(HtmlConstants.CSS_FONT_SIZE, fontSize, false)); } if (HtmlConstants.isMajorFont(firstFont.getCssText())) { style.getProperties().add(i, new Property(HtmlConstants.CSS_FONT_FAMILY, firstFont, false)); } else { for (j++; j < length; j++) { CSSValue fontFamily = valueList.item(j); if (HtmlConstants.isMajorFont(fontFamily.getCssText())) { style.getProperties().add(i, new Property(HtmlConstants.CSS_FONT_FAMILY, fontFamily, false)); break; } } } break; } else if (j == length - 1) { // font-family在font中一定是最后出现 style.getProperties().add(i, new Property(HtmlConstants.CSS_FONT_FAMILY, item, false)); } } } private static void splitBox(CSSValueImpl valueList, int length, CSSStyleDeclarationImpl style, int i, BoxProperty boxProperty) { switch (length) { // 当仅一个值时实际返回长度为0 case 0: case 1: if (StringUtils.isNotBlank(valueList.getCssText())) { boxProperty.setValues(style, i, valueList); } break; case 2: boxProperty.setValues(style, i, valueList.item(0), valueList.item(1)); break; case 3: boxProperty.setValues(style, i, valueList.item(0), valueList.item(1), valueList.item(2)); break; case 4: boxProperty.setValues(style, i, valueList.item(0), valueList.item(1), valueList.item(2), valueList.item(3)); break; } } private static void splitListStyle(CSSValueImpl valueList, int length, CSSStyleDeclarationImpl style, int i) { switch (length) { case 0: case 1: String cssText = valueList.getCssText(); if (StringUtils.isNotBlank(cssText)) { handleListStyleValue(style, i, cssText.toLowerCase(), valueList); } break; default: for (int j = 0; j < length; j++) { CSSValue item = valueList.item(j); String value = item.getCssText().toLowerCase(); handleListStyleValue(style, i, value, item); } } } private static void handleListStyleValue(CSSStyleDeclarationImpl style, int i, String value, CSSValue item) { if (HtmlConstants.LIST_STYLE_POSITIONS.contains(value)) { style.getProperties().add(i, new Property(HtmlConstants.CSS_LIST_STYLE_POSITION, item, false)); } else if (!value.contains(HtmlConstants.LEFT_PARENTHESIS)) { style.getProperties().add(i, new Property(HtmlConstants.CSS_LIST_STYLE_TYPE, item, false)); } } private static void splitTextDecoration(CSSValueImpl valueList, int length, CSSStyleDeclarationImpl style, int i) { if (length == 0) { style.getProperties().add(i, new Property(HtmlConstants.CSS_TEXT_DECORATION_LINE, valueList, false)); return; } StringBuilder lines = new StringBuilder(22); for (int j = 0; j < length; j++) { CSSValue item = valueList.item(j); String value = item.getCssText(); String lowerCase = value.toLowerCase(); if (HtmlConstants.NONE.equals(lowerCase)) { style.getProperties().add(i, new Property(HtmlConstants.CSS_TEXT_DECORATION_LINE, item, false)); break; } if (HtmlConstants.TEXT_DECORATION_LINES.contains(lowerCase)) { if (lines.length() > 0) { lines.append(' '); } lines.append(lowerCase); } else if (HtmlConstants.TEXT_DECORATION_STYLES.contains(lowerCase)) { style.getProperties().add(i, new Property(HtmlConstants.CSS_TEXT_DECORATION_STYLE, item, false)); } else if (Colors.maybe(lowerCase)) { style.getProperties().add(i, new Property(HtmlConstants.CSS_TEXT_DECORATION_COLOR, item, false)); } } if (lines.length() > 0) { style.getProperties().add(i, newProperty(HtmlConstants.CSS_TEXT_DECORATION_LINE, lines.toString())); } } } ================================================ FILE: src/main/java/org/ddr/poi/html/util/Colors.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.html.util; import org.apache.commons.lang3.StringUtils; import org.ddr.poi.html.HtmlConstants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.HashMap; import java.util.Map; /** * 颜色工具类 * * @author Draco * @since 2021-02-23 */ public class Colors { private static final Logger log = LoggerFactory.getLogger(Colors.class); private static final Map COLOR_MAP = new HashMap<>(160); public static final String BLACK = "000000"; public static final String WHITE = "FFFFFF"; public static final String DEFAULT_COLOR = BLACK; public static final String TRANSPARENT = "transparent"; // https://developer.mozilla.org/zh-CN/docs/Web/CSS/color_value#%E8%89%B2%E5%BD%A9%E5%85%B3%E9%94%AE%E5%AD%97 static { COLOR_MAP.put("black", BLACK); COLOR_MAP.put("silver", "C0C0C0"); COLOR_MAP.put("gray", "808080"); COLOR_MAP.put("white", "FFFFFF"); COLOR_MAP.put("maroon", "800000"); COLOR_MAP.put("red", "FF0000"); COLOR_MAP.put("purple", "800080"); COLOR_MAP.put("fuchsia", "FF00FF"); COLOR_MAP.put("green", "008000"); COLOR_MAP.put("lime", "00FF00"); COLOR_MAP.put("olive", "808000"); COLOR_MAP.put("yellow", "FFFF00"); COLOR_MAP.put("navy", "000080"); COLOR_MAP.put("blue", "0000FF"); COLOR_MAP.put("teal", "008080"); COLOR_MAP.put("aqua", "00FFFF"); COLOR_MAP.put("orange", "FFA500"); COLOR_MAP.put("aliceblue", "F0F8FF"); COLOR_MAP.put("antiquewhite", "FAEBD7"); COLOR_MAP.put("aquamarine", "7FFFD4"); COLOR_MAP.put("azure", "F0FFFF"); COLOR_MAP.put("beige", "F5F5DC"); COLOR_MAP.put("bisque", "FFE4C4"); COLOR_MAP.put("blanchedalmond", "FFEBCD"); COLOR_MAP.put("blueviolet", "8A2BE2"); COLOR_MAP.put("brown", "A52A2A"); COLOR_MAP.put("burlywood", "DEB887"); COLOR_MAP.put("cadetblue", "5F9EA0"); COLOR_MAP.put("chartreuse", "7FFF00"); COLOR_MAP.put("chocolate", "D2691E"); COLOR_MAP.put("coral", "FF7F50"); COLOR_MAP.put("cornflowerblue", "6495ED"); COLOR_MAP.put("cornsilk", "FFF8DC"); COLOR_MAP.put("crimson", "DC143C"); COLOR_MAP.put("darkblue", "00008B"); COLOR_MAP.put("darkcyan", "008B8B"); COLOR_MAP.put("darkgoldenrod", "B8860B"); COLOR_MAP.put("darkgray", "A9A9A9"); COLOR_MAP.put("darkgreen", "006400"); COLOR_MAP.put("darkgrey", "A9A9A9"); COLOR_MAP.put("darkkhaki", "BDB76B"); COLOR_MAP.put("darkmagenta", "8B008B"); COLOR_MAP.put("darkolivegreen", "556B2F"); COLOR_MAP.put("darkorange", "FF8C00"); COLOR_MAP.put("darkorchid", "9932CC"); COLOR_MAP.put("darkred", "8B0000"); COLOR_MAP.put("darksalmon", "E9967A"); COLOR_MAP.put("darkseagreen", "8FBC8F"); COLOR_MAP.put("darkslateblue", "483D8B"); COLOR_MAP.put("darkslategray", "2F4F4F"); COLOR_MAP.put("darkslategrey", "2F4F4F"); COLOR_MAP.put("darkturquoise", "00CED1"); COLOR_MAP.put("darkviolet", "9400D3"); COLOR_MAP.put("deeppink", "FF1493"); COLOR_MAP.put("deepskyblue", "00BFFF"); COLOR_MAP.put("dimgray", "696969"); COLOR_MAP.put("dimgrey", "696969"); COLOR_MAP.put("dodgerblue", "1E90FF"); COLOR_MAP.put("firebrick", "B22222"); COLOR_MAP.put("floralwhite", "FFFAF0"); COLOR_MAP.put("forestgreen", "228B22"); COLOR_MAP.put("gainsboro", "DCDCDC"); COLOR_MAP.put("ghostwhite", "F8F8FF"); COLOR_MAP.put("gold", "FFD700"); COLOR_MAP.put("goldenrod", "DAA520"); COLOR_MAP.put("greenyellow", "ADFF2F"); COLOR_MAP.put("grey", "808080"); COLOR_MAP.put("honeydew", "F0FFF0"); COLOR_MAP.put("hotpink", "FF69B4"); COLOR_MAP.put("indianred", "CD5C5C"); COLOR_MAP.put("indigo", "4B0082"); COLOR_MAP.put("ivory", "FFFFF0"); COLOR_MAP.put("khaki", "F0E68C"); COLOR_MAP.put("lavender", "E6E6FA"); COLOR_MAP.put("lavenderblush", "FFF0F5"); COLOR_MAP.put("lawngreen", "7CFC00"); COLOR_MAP.put("lemonchiffon", "FFFACD"); COLOR_MAP.put("lightblue", "ADD8E6"); COLOR_MAP.put("lightcoral", "F08080"); COLOR_MAP.put("lightcyan", "E0FFFF"); COLOR_MAP.put("lightgoldenrodyellow", "FAFAD2"); COLOR_MAP.put("lightgray", "D3D3D3"); COLOR_MAP.put("lightgreen", "90EE90"); COLOR_MAP.put("lightgrey", "D3D3D3"); COLOR_MAP.put("lightpink", "FFB6C1"); COLOR_MAP.put("lightsalmon", "FFA07A"); COLOR_MAP.put("lightseagreen", "20B2AA"); COLOR_MAP.put("lightskyblue", "87CEFA"); COLOR_MAP.put("lightslategray", "778899"); COLOR_MAP.put("lightslategrey", "778899"); COLOR_MAP.put("lightsteelblue", "B0C4DE"); COLOR_MAP.put("lightyellow", "FFFFE0"); COLOR_MAP.put("limegreen", "32CD32"); COLOR_MAP.put("linen", "FAF0E6"); COLOR_MAP.put("mediumaquamarine", "66CDAA"); COLOR_MAP.put("mediumblue", "0000CD"); COLOR_MAP.put("mediumorchid", "BA55D3"); COLOR_MAP.put("mediumpurple", "9370DB"); COLOR_MAP.put("mediumseagreen", "3CB371"); COLOR_MAP.put("mediumslateblue", "7B68EE"); COLOR_MAP.put("mediumspringgreen", "00FA9A"); COLOR_MAP.put("mediumturquoise", "48D1CC"); COLOR_MAP.put("mediumvioletred", "C71585"); COLOR_MAP.put("midnightblue", "191970"); COLOR_MAP.put("mintcream", "F5FFFA"); COLOR_MAP.put("mistyrose", "FFE4E1"); COLOR_MAP.put("moccasin", "FFE4B5"); COLOR_MAP.put("navajowhite", "FFDEAD"); COLOR_MAP.put("oldlace", "FDF5E6"); COLOR_MAP.put("olivedrab", "6B8E23"); COLOR_MAP.put("orangered", "FF4500"); COLOR_MAP.put("orchid", "DA70D6"); COLOR_MAP.put("palegoldenrod", "EEE8AA"); COLOR_MAP.put("palegreen", "98FB98"); COLOR_MAP.put("paleturquoise", "AFEEEE"); COLOR_MAP.put("palevioletred", "DB7093"); COLOR_MAP.put("papayawhip", "FFEFD5"); COLOR_MAP.put("peachpuff", "FFDAB9"); COLOR_MAP.put("peru", "CD853F"); COLOR_MAP.put("pink", "FFC0CB"); COLOR_MAP.put("plum", "DDA0DD"); COLOR_MAP.put("powderblue", "B0E0E6"); COLOR_MAP.put("rosybrown", "BC8F8F"); COLOR_MAP.put("royalblue", "4169E1"); COLOR_MAP.put("saddlebrown", "8B4513"); COLOR_MAP.put("salmon", "FA8072"); COLOR_MAP.put("sandybrown", "F4A460"); COLOR_MAP.put("seagreen", "2E8B57"); COLOR_MAP.put("seashell", "FFF5EE"); COLOR_MAP.put("sienna", "A0522D"); COLOR_MAP.put("skyblue", "87CEEB"); COLOR_MAP.put("slateblue", "6A5ACD"); COLOR_MAP.put("slategray", "708090"); COLOR_MAP.put("slategrey", "708090"); COLOR_MAP.put("snow", "FFFAFA"); COLOR_MAP.put("springgreen", "00FF7F"); COLOR_MAP.put("steelblue", "4682B4"); COLOR_MAP.put("tan", "D2B48C"); COLOR_MAP.put("thistle", "D8BFD8"); COLOR_MAP.put("tomato", "FF6347"); COLOR_MAP.put("turquoise", "40E0D0"); COLOR_MAP.put("violet", "EE82EE"); COLOR_MAP.put("wheat", "F5DEB3"); COLOR_MAP.put("whitesmoke", "F5F5F5"); COLOR_MAP.put("yellowgreen", "9ACD32"); COLOR_MAP.put("rebeccapurple", "663399"); } /** * 根据颜色名称获取颜色值,未找到时返回null * * @param name 颜色名称 * @return 颜色值 */ public static String getColorByName(String name) { return name == null ? null : COLOR_MAP.get(name.toLowerCase()); } /** * 根据颜色名称获取颜色值,未找到时返回默认颜色值 * * @param name 颜色名称 * @param defaultColor 默认颜色值 * @return 颜色值 */ public static String getColorByName(String name, String defaultColor) { return name == null ? defaultColor : COLOR_MAP.getOrDefault(name.toLowerCase(), defaultColor); } /** * hsl颜色转换为颜色值 link * * @param h hue 0 - 360 * @param s saturation 0 - 100 * @param l lightness 0 - 100 * @return 颜色值 */ public static String fromHSL(float h, float s, float l) { float[] rgb = hsl2rgb(h, s, l); return toHexString(toInt(rgb[0]), toInt(rgb[1]), toInt(rgb[2])); } private static float[] hsl2rgb(float h, float s, float l) { h = h % 360; if (h < 0) { h += 360; } s /= 100; l /= 100; return new float[] { hue2rgb(h, s, l, 0), hue2rgb(h, s, l, 8), hue2rgb(h, s, l, 4) }; } private static float hue2rgb(float h, float s, float l, int n) { float k = (n + h / 30) % 12; float a = s * Math.min(l, 1 - l); return l - a * Math.max(-1, Math.min(k - 3, Math.min(9 - k, 1))); } /** * hwb颜色转换为颜色值 link * * @param h hue 0 - 360 * @param w whiteness 0 - 100 * @param b blackness 0 - 100 * @return 颜色值 */ public static String fromHWB(float h, float w, float b) { w /= 100; b /= 100; if (w + b >= 1) { int gray = toInt(w / (w + b)); return toHexString(gray, gray, gray); } float[] rgb = hsl2rgb(h, 100, 50); for (int i = 0; i < 3; i++) { rgb[i] *= (1 - w - b); rgb[i] += w; } return toHexString(toInt(rgb[0]), toInt(rgb[1]), toInt(rgb[2])); } private static int toInt(float f) { return (int) (f * 255 + 0.5); } private static float degrees(String s) { if (s.endsWith("grad")) { return Float.parseFloat(s.substring(0, s.length() - 4)) * 0.9f; } else if (s.endsWith("rad")) { return (float) (Double.parseDouble(s.substring(0, s.length() - 3)) * 180 / Math.PI); } else if (s.endsWith("turn")) { return Float.parseFloat(s.substring(0, s.length() - 4)) * 360; } else if (s.endsWith("deg")) { return Float.parseFloat(s.substring(0, s.length() - 3)); } else { return Float.parseFloat(s); } } /** * 将RGB转换为颜色值 * * @param r Red * @param g Green * @param b Blue * @return 颜色值 */ public static String toHexString(int r, int g, int b) { return String.format("%02X%02X%02X", r, g, b); } /** * 解析样式值为颜色值 * * @param style 样式值 * @param defaultColor 默认颜色值 * @return 颜色值 */ public static String fromStyle(String style, String defaultColor) { if (StringUtils.isBlank(style)) { return defaultColor; } // Word中不支持alpha通道,直接忽略 if (style.startsWith(HtmlConstants.SHARP)) { String hex = style.substring(1).trim(); if (hex.length() == 3 || hex.length() == 4) { char[] chars = new char[6]; for (int i = 0; i < 6; i++) { chars[i] = hex.charAt(i >> 1); } return String.valueOf(chars); } else if (hex.length() >= 6) { return hex.substring(0, 6); } else { warn(style); } } else if (style.startsWith("rgb")) { // color: rgb(34, 12, 64, 0.6); // color: rgba(34, 12, 64, 0.6); // color: rgb(34 12 64 / 0.6); // color: rgba(34 12 64 / 0.3); // color: rgb(34.0 12 64 / 60%); // color: rgba(34.6 12 64 / 30%); String[] array = StringUtils.split(StringUtils.substringBetween(style, "(", ")"), ", /"); if (array != null && array.length >= 3) { try { return toHexString((int) Float.parseFloat(array[0]), (int) Float.parseFloat(array[1]), (int) Float.parseFloat(array[2])); } catch (NumberFormatException e) { warn(style); } } else { warn(style); } } else if (style.startsWith("hsl")) { // color: hsl(30, 100%, 50%, 0.6); // color: hsla(30, 100%, 50%, 0.6); // color: hsl(30 100% 50% / 0.6); // color: hsla(30 100% 50% / 0.6); // color: hsl(30.0 100% 50% / 60%); // color: hsla(30.2 100% 50% / 60%); String[] array = StringUtils.split(StringUtils.substringBetween(style, "(", ")"), ", /"); if (array != null && array.length >= 3 && array[1].endsWith(HtmlConstants.PERCENT) && array[2].endsWith(HtmlConstants.PERCENT)) { float h = degrees(array[0]); String ss = array[1]; float s = Float.parseFloat(ss.substring(0, ss.length() - 1)) / 100; String ls = array[2]; float l = Float.parseFloat(ls.substring(0, ls.length() - 1)) / 100; return fromHSL(h, s, l); } else { warn(style); } } else if (style.startsWith("hwb")) { // color: hwb(90 10% 10%); // color: hwb(90 10% 10% / 0.5); // color: hwb(90deg 10% 10%); // color: hwb(1.5708rad 60% 0%); // color: hwb(0.25turn 0% 40% / 50%); String[] array = StringUtils.split(StringUtils.substringBetween(style, "(", ")"), ", /"); if (array != null && array.length >= 3 && array[1].endsWith(HtmlConstants.PERCENT) && array[2].endsWith(HtmlConstants.PERCENT)) { float h = degrees(array[0]); String ws = array[1]; float w = Float.parseFloat(ws.substring(0, ws.length() - 1)) / 100; String bs = array[2]; float b = Float.parseFloat(bs.substring(0, bs.length() - 1)) / 100; return fromHWB(h, w, b); } else { warn(style); } } else { return getColorByName(style, defaultColor); } return defaultColor; } /** * 解析样式值为颜色值,解析失败时返回默认颜色值(黑色) * * @param style 样式值 * @return 颜色值 */ public static String fromStyle(String style) { return fromStyle(style, DEFAULT_COLOR); } private static void warn(String style) { log.warn("Illegal color: {}", style); } /** * 判断样式值是否可能为颜色 * * @param style 样式值 * @return 是否可能为颜色 */ public static boolean maybe(String style) { return style.startsWith(HtmlConstants.SHARP) || style.startsWith("rgb") || style.startsWith("hsl") || style.startsWith("hwb") || COLOR_MAP.containsKey(style); } } ================================================ FILE: src/main/java/org/ddr/poi/html/util/ColumnStyle.java ================================================ package org.ddr.poi.html.util; import com.steadystate.css.dom.CSSStyleDeclarationImpl; /** * 表格列样式定义 * * @author Draco * @since 2022-10-28 */ public class ColumnStyle { private CSSStyleDeclarationImpl style; private CSSLength width; public ColumnStyle(CSSStyleDeclarationImpl style, CSSLength width) { this.style = style; this.width = width; } public CSSStyleDeclarationImpl getStyle() { return style; } public CSSLength getWidth() { return width; } } ================================================ FILE: src/main/java/org/ddr/poi/html/util/EmptyCSSStyle.java ================================================ package org.ddr.poi.html.util; import com.steadystate.css.dom.CSSStyleDeclarationImpl; import com.steadystate.css.dom.Property; import org.w3c.dom.DOMException; import java.util.Collections; import java.util.List; /** * 空样式 * * @author Draco * @since 2022-10-21 */ public class EmptyCSSStyle extends CSSStyleDeclarationImpl { @Override public void setProperties(List properties) { throw new UnsupportedOperationException(); } @Override public List getProperties() { return Collections.unmodifiableList(super.getProperties()); } @Override public void setProperty(String propertyName, String value, String priority) throws DOMException { throw new UnsupportedOperationException(); } @Override public void setCssText(String cssText) throws DOMException { throw new UnsupportedOperationException(); } } ================================================ FILE: src/main/java/org/ddr/poi/html/util/InlineStyle.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.html.util; import com.steadystate.css.dom.CSSStyleDeclarationImpl; /** * 行内样式封装类 * * @author Draco * @since 2021-03-18 */ public final class InlineStyle { /** * 样式声明 */ private final CSSStyleDeclarationImpl declaration; /** * 是否为区块元素 */ private final boolean block; public InlineStyle(CSSStyleDeclarationImpl declaration, boolean block) { this.declaration = declaration; this.block = block; } public CSSStyleDeclarationImpl getDeclaration() { return this.declaration; } public boolean isBlock() { return this.block; } } ================================================ FILE: src/main/java/org/ddr/poi/html/util/JsoupUtils.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.html.util; import org.ddr.poi.html.HtmlConstants; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.nodes.Node; import org.jsoup.parser.CustomHtmlTreeBuilder; import org.jsoup.parser.Parser; import org.jsoup.select.Elements; import java.util.Arrays; import java.util.HashSet; import java.util.Set; import java.util.function.Predicate; /** * JSoup工具类 * * @author Draco * @since 2021-03-03 */ public class JsoupUtils { /** * 选取符合条件的子元素到目标集合中 * * @param collection 目标集合 * @param parent 父元素 * @param predicate 条件 */ public static void selectChildren(Elements collection, Element parent, Predicate predicate) { for (Node node : parent.childNodes()) { if (node instanceof Element) { Element child = ((Element) node); if (predicate.test(child)) { collection.add(child); } } } } /** * 选取指定标签的子元素 * * @param parent 父元素 * @param tag 标签名称,小写 * @return 子元素集合 */ public static Elements children(Element parent, String tag) { Elements elements = new Elements(); selectChildren(elements, parent, c -> c.normalName().equals(tag)); return elements; } /** * 选取指定标签的子元素 * * @param parent 父元素 * @param tags 多种标签名称,小写 * @return 子元素集合 */ public static Elements children(Element parent, String... tags) { Elements elements = new Elements(); Set targets = new HashSet<>(Arrays.asList(tags)); selectChildren(elements, parent, c -> targets.contains(c.normalName())); return elements; } /** * 选取第一个指定标签的子元素 * * @param parent 父元素 * @param tag 标签名称,小写 * @return 子元素 */ public static Element firstChild(Element parent, String tag) { for (Node node : parent.childNodes()) { if (node instanceof Element) { Element child = ((Element) node); if (child.normalName().equals(tag)) { return child; } } } return null; } /** * 选取表格的所有行元素 * * @param parent 表格元素 * @return 行元素集合 */ public static Elements childRows(Element parent) { Elements elements = new Elements(); for (Node node : parent.childNodes()) { if (node instanceof Element) { Element child = ((Element) node); if (HtmlConstants.TAG_TR.equals(child.normalName())) { // 直接位于table标签下 elements.add(child); } else { // 可能位于thead/tbody/tfoot标签下,选取直接子元素避免受嵌套表格影响 selectChildren(elements, child, c -> HtmlConstants.TAG_TR.equals(c.normalName())); } } } return elements; } /** * @see org.jsoup.Jsoup#parseBodyFragment(String) * @see org.jsoup.parser.Parser#parseBodyFragment(String, String) */ public static Document parse(String html) { CustomHtmlTreeBuilder treeBuilder = new CustomHtmlTreeBuilder(); return Jsoup.parse(html, new Parser(treeBuilder)); } } ================================================ FILE: src/main/java/org/ddr/poi/html/util/ListStyle.java ================================================ package org.ddr.poi.html.util; /** * 列表样式 * * @author Draco * @since 2024-09-12 */ public class ListStyle { private final ListStyleType numberFormat; private final boolean hanging; private final int left; private final int right; public ListStyle(ListStyleType numberFormat, boolean hanging, int left, int right) { this.numberFormat = numberFormat; this.hanging = hanging; this.left = left; this.right = right; } public ListStyleType getNumberFormat() { return numberFormat; } public boolean isHanging() { return hanging; } public int getLeft() { return left; } public int getRight() { return right; } @Override public String toString() { return "ListStyle{" + "numberFormat=" + numberFormat + ", hanging=" + hanging + ", left=" + left + ", right=" + right + '}'; } } ================================================ FILE: src/main/java/org/ddr/poi/html/util/ListStyleType.java ================================================ package org.ddr.poi.html.util; import org.openxmlformats.schemas.wordprocessingml.x2006.main.STNumberFormat; import java.util.Arrays; import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; /** * 列表项样式 * * @author Draco * @since 2022-02-08 */ public interface ListStyleType { String SYMBOL_DISC = "\uF06C"; // •⚫ String SYMBOL_CIRCLE = "\uF0A1"; // ◦⚪ String SYMBOL_DISCLOSURE_CLOSED = "\uF075"; // ▸ String SYMBOL_DISCLOSURE_OPEN = "\uF071"; // ▾ String SYMBOL_SQUARE = "\uF06E"; // ▪ String FONT_WINGDINGS = "Wingdings"; String FONT_WINGDINGS_3 = "Wingdings 3"; String getName(); STNumberFormat.Enum getFormat(); /** * string: Custom symbol empty string: System counting style null: No symbol */ String getText(); String getFont(); enum Unordered implements ListStyleType { DISC("disc", STNumberFormat.BULLET, SYMBOL_DISC, FONT_WINGDINGS), CIRCLE("circle", STNumberFormat.BULLET, SYMBOL_CIRCLE, FONT_WINGDINGS), DECIMAL("decimal", STNumberFormat.DECIMAL, "", null), DISCLOSURE_CLOSED("disclosure-closed", STNumberFormat.BULLET, SYMBOL_DISCLOSURE_CLOSED, FONT_WINGDINGS_3), DISCLOSURE_OPEN("disclosure-open", STNumberFormat.BULLET, SYMBOL_DISCLOSURE_OPEN, FONT_WINGDINGS_3), SQUARE("square", STNumberFormat.BULLET, SYMBOL_SQUARE, FONT_WINGDINGS), NONE("none", STNumberFormat.NONE, null, null); private static final Map TYPE_MAP = Arrays.stream(values()) .collect(Collectors.toMap(Unordered::getName, Function.identity())); private final String name; private final STNumberFormat.Enum format; private final String text; private final String font; Unordered(String name, STNumberFormat.Enum format, String text, String font) { this.name = name; this.format = format; this.text = text; this.font = font; } @Override public String getName() { return name; } @Override public STNumberFormat.Enum getFormat() { return format; } @Override public String getText() { return text; } @Override public String getFont() { return font; } public static ListStyleType of(String type) { return TYPE_MAP.getOrDefault(type, DISC); } } /** * https://www.w3.org/TR/css-counter-styles-3/#predefined-counters */ enum Ordered implements ListStyleType { /* Numeric */ DECIMAL("decimal", STNumberFormat.DECIMAL, "", null), DECIMAL_LEADING_ZERO("decimal-leading-zero", STNumberFormat.DECIMAL_ZERO, "", null), CJK_DECIMAL("cjk-decimal", STNumberFormat.TAIWANESE_DIGITAL, "", null), HEBREW("hebrew", STNumberFormat.HEBREW_1, "", null), LOWER_ROMAN("lower-roman", STNumberFormat.LOWER_ROMAN, "", null), UPPER_ROMAN("upper-roman", STNumberFormat.UPPER_ROMAN, "", null), THAI("thai", STNumberFormat.THAI_NUMBERS, "", null), /* Alphabetic */ LOWER_ALPHA("lower-alpha", STNumberFormat.LOWER_LETTER, "", null), LOWER_LATIN("lower-latin", STNumberFormat.LOWER_LETTER, "", null), UPPER_ALPHA("upper-alpha", STNumberFormat.UPPER_LETTER, "", null), UPPER_LATIN("upper-latin", STNumberFormat.UPPER_LETTER, "", null), // hiragana // hiragana-iroha KATAKANA("katakana", STNumberFormat.AIUEO_FULL_WIDTH, "", null), KATAKANA_IROHA("katakana-iroha", STNumberFormat.IROHA_FULL_WIDTH, "", null), /* Symbolic */ DISC("disc", STNumberFormat.BULLET, SYMBOL_DISC, FONT_WINGDINGS), CIRCLE("circle", STNumberFormat.BULLET, SYMBOL_CIRCLE, FONT_WINGDINGS), DISCLOSURE_CLOSED("disclosure-closed", STNumberFormat.BULLET, SYMBOL_DISCLOSURE_CLOSED, FONT_WINGDINGS_3), DISCLOSURE_OPEN("disclosure-open", STNumberFormat.BULLET, SYMBOL_DISCLOSURE_OPEN, FONT_WINGDINGS_3), SQUARE("square", STNumberFormat.BULLET, SYMBOL_SQUARE, FONT_WINGDINGS), /* Longhand East Asian */ JAPANESE_INFORMAL("japanese-informal", STNumberFormat.JAPANESE_COUNTING, "", null), JAPANESE_FORMAL("japanese-formal", STNumberFormat.JAPANESE_LEGAL, "", null), KOREAN_HANGUL_FORMAL("korean-hangul-formal", STNumberFormat.KOREAN_COUNTING, "", null), // partial matching KOREAN_HANJA_INFORMAL("korean-hanja-informal", STNumberFormat.KOREAN_DIGITAL_2, "", null), // partial matching KOREAN_HANJA_FORMAL("korean-hanja-formal", STNumberFormat.CHINESE_LEGAL_SIMPLIFIED, "", null), SIMP_CHINESE_INFORMAL("simp-chinese-informal", STNumberFormat.CHINESE_COUNTING, "", null), SIMP_CHINESE_FORMAL("simp-chinese-formal", STNumberFormat.CHINESE_LEGAL_SIMPLIFIED, "", null), TRAD_CHINESE_INFORMAL("trad-chinese-informal", STNumberFormat.TAIWANESE_COUNTING, "", null), // partial matching TRAD_CHINESE_FORMAL("trad-chinese-formal", STNumberFormat.CHINESE_LEGAL_SIMPLIFIED, "", null), CJK_IDEOGRAPHIC("cjk-ideographic", STNumberFormat.CHINESE_LEGAL_SIMPLIFIED, "", null), /* ol type */ ONE("1", STNumberFormat.DECIMAL, "", null), LOWER_A("a", STNumberFormat.LOWER_LETTER, "", null), UPPER_A("A", STNumberFormat.UPPER_LETTER, "", null), LOWER_I("i", STNumberFormat.LOWER_ROMAN, "", null), UPPER_I("I", STNumberFormat.UPPER_ROMAN, "", null), NONE("none", STNumberFormat.NONE, null, null); private static final Map TYPE_MAP = Arrays.stream(values()) .collect(Collectors.toMap(Ordered::getName, Function.identity())); private final String name; private final STNumberFormat.Enum format; private final String text; private final String font; Ordered(String name, STNumberFormat.Enum format, String text, String font) { this.name = name; this.format = format; this.text = text; this.font = font; } @Override public String getName() { return name; } @Override public STNumberFormat.Enum getFormat() { return format; } @Override public String getText() { return text; } @Override public String getFont() { return font; } public static ListStyleType of(String type) { return TYPE_MAP.getOrDefault(type, DECIMAL); } } } ================================================ FILE: src/main/java/org/ddr/poi/html/util/NamedBorderWidth.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.html.util; import org.ddr.poi.html.HtmlConstants; import java.util.Arrays; import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; /** * 已命名的边框宽度,宽度值与chrome一致 * * @author Draco * @since 2021-03-29 */ public enum NamedBorderWidth { THIN(HtmlConstants.THIN, 1), MEDIUM(HtmlConstants.MEDIUM, 3), THICK(HtmlConstants.THICK, 5); private final String name; private final CSSLength width; private static final Map NAMED_MAP = Arrays.stream(NamedBorderWidth.values()).collect( Collectors.toMap(NamedBorderWidth::getName, Function.identity()) ); NamedBorderWidth(String name, int px) { this.name = name; this.width = new CSSLength(px, CSSLengthUnit.PX); } public String getName() { return name; } public CSSLength getWidth() { return width; } public static NamedBorderWidth of(String name) { return NAMED_MAP.get(name); } public static boolean contains(String name) { return NAMED_MAP.containsKey(name); } } ================================================ FILE: src/main/java/org/ddr/poi/html/util/NamedFontSize.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.html.util; import org.ddr.poi.html.HtmlConstants; import java.util.Arrays; import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; /** * 已命名的字号 * * @author Draco * @since 2021-02-25 */ public enum NamedFontSize { XX_SMALL(HtmlConstants.XX_SMALL, 6d), X_SMALL(HtmlConstants.X_SMALL, 7.5d), SMALL(HtmlConstants.SMALL, 10d), MEDIUM(HtmlConstants.MEDIUM, 12d), LARGE(HtmlConstants.LARGE, 13.5d), X_LARGE(HtmlConstants.X_LARGE, 18d), XX_LARGE(HtmlConstants.XX_LARGE, 24d), XXX_LARGE(HtmlConstants.XXX_LARGE, 36d); private final String name; private final CSSLength size; private static final Map NAMED_MAP = Arrays.stream(NamedFontSize.values()).collect( Collectors.toMap(NamedFontSize::getName, Function.identity()) ); NamedFontSize(String name, double pt) { this.name = name; this.size = new CSSLength(pt, CSSLengthUnit.PT); } public String getName() { return name; } public CSSLength getSize() { return size; } public static NamedFontSize of(String name) { return NAMED_MAP.get(name); } } ================================================ FILE: src/main/java/org/ddr/poi/html/util/NumberingContext.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.html.util; import org.apache.commons.lang3.StringUtils; import org.apache.poi.xwpf.usermodel.BodyType; import org.apache.poi.xwpf.usermodel.XWPFAbstractNum; import org.apache.poi.xwpf.usermodel.XWPFDocument; import org.apache.poi.xwpf.usermodel.XWPFNumbering; import org.apache.poi.xwpf.usermodel.XWPFParagraph; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTAbstractNum; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTFonts; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTInd; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTLvl; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPPr; import org.openxmlformats.schemas.wordprocessingml.x2006.main.STHint; import org.openxmlformats.schemas.wordprocessingml.x2006.main.STJc; import org.openxmlformats.schemas.wordprocessingml.x2006.main.STLevelSuffix; import org.openxmlformats.schemas.wordprocessingml.x2006.main.STMultiLevelType; import java.math.BigInteger; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.TreeMap; /** * 列表上下文 * * @author Draco * @since 2021-02-19 */ public class NumberingContext { /** * 每级缩进 */ private static final int INDENT = 360; private static final int HANGING = 360; private final XWPFDocument document; private int indent = INDENT; private int hanging = HANGING; private STLevelSuffix.Enum spacing; private int nextAbstractNumberId; private int nextNumberingLevel; private List listStyles; private final TreeMap numberIdMap = new TreeMap<>(Collections.reverseOrder()); private List numberingParagraphs; public NumberingContext(XWPFDocument document) { this.document = document; } /** * 开始新的列表 * * @param listStyle 列表样式 */ public void startLevel(ListStyle listStyle) { int level = nextNumberingLevel++; if (level == 0) { numberingParagraphs = new ArrayList<>(8); listStyles = new ArrayList<>(4); } listStyles.add(listStyle); } /** * 结束当前列表 */ public void endLevel() { nextNumberingLevel--; String key = getFormatKey(); BigInteger numberId = getNumberId(key); BigInteger currentLevel = BigInteger.valueOf(nextNumberingLevel); for (int i = numberingParagraphs.size() - 1; i >= 0; i--) { XWPFParagraph paragraph = numberingParagraphs.get(i); if (currentLevel.equals(paragraph.getNumIlvl())) { paragraph.setNumID(numberId); numberingParagraphs.remove(i); } else { break; } } if (!listStyles.isEmpty()) { listStyles.remove(listStyles.size() - 1); } if (nextNumberingLevel == 0) { numberingParagraphs = null; listStyles = null; numberIdMap.clear(); } } /** * 新增段落 * * @param paragraph 段落 */ public void add(XWPFParagraph paragraph) { if (numberingParagraphs == null) { // startLevel method not called, fallback as
or

return; } paragraph.setNumILvl(BigInteger.valueOf(nextNumberingLevel - 1)); if (paragraph.getPartType() == BodyType.TABLECELL) { CTPPr pPr = RenderUtils.getPPr(paragraph.getCTP()); if (!pPr.isSetInd()) { RenderUtils.getInd(pPr).setFirstLine(BigInteger.ZERO); } } numberingParagraphs.add(paragraph); } /** * 获取列表ID * * @param key Key * @return 列表ID */ private BigInteger getNumberId(String key) { BigInteger numberId = null; for (Map.Entry entry : numberIdMap.entrySet()) { if (entry.getKey().startsWith(key)) { numberId = entry.getValue(); break; } } if (numberId == null) { XWPFNumbering numbering = document.createNumbering(); while (true) { BigInteger abstractNumberId = BigInteger.valueOf(nextAbstractNumberId++); XWPFAbstractNum abstractNum = numbering.getAbstractNum(abstractNumberId); if (abstractNum == null) { CTAbstractNum ctAbstractNum = CTAbstractNum.Factory.newInstance(); ctAbstractNum.setAbstractNumId(abstractNumberId); ctAbstractNum.addNewMultiLevelType().setVal(STMultiLevelType.HYBRID_MULTILEVEL); for (int i = 0; i < listStyles.size(); i++) { ListStyle listStyle = listStyles.get(i); ListStyleType listStyleType = listStyle.getNumberFormat(); CTLvl cTLvl = ctAbstractNum.addNewLvl(); CTInd ind = cTLvl.addNewPPr().addNewInd(); long left = indent * i + listStyle.getLeft(); long right = listStyle.getRight(); for (int j = 0; j < i; j++) { ListStyle previous = listStyles.get(j); left += previous.getLeft(); right += previous.getRight(); } ind.setLeft(BigInteger.valueOf(left)); ind.setRight(BigInteger.valueOf(right)); if (listStyle.isHanging()) { ind.setHanging(BigInteger.valueOf(hanging)); } cTLvl.addNewNumFmt().setVal(listStyleType.getFormat()); cTLvl.addNewLvlText().setVal(getLevelText(listStyleType, i)); cTLvl.addNewStart().setVal(BigInteger.ONE); cTLvl.setIlvl(BigInteger.valueOf(i)); cTLvl.addNewLvlJc().setVal(STJc.LEFT); if (StringUtils.isNotBlank(listStyleType.getFont())) { CTFonts ctFonts = cTLvl.addNewRPr().addNewRFonts(); ctFonts.setAscii(listStyleType.getFont()); ctFonts.setHAnsi(listStyleType.getFont()); ctFonts.setHint(STHint.DEFAULT); } if (spacing != null) { cTLvl.addNewSuff().setVal(spacing); } } numbering.addAbstractNum(new XWPFAbstractNum(ctAbstractNum, numbering)); numberId = numbering.addNum(abstractNumberId); numberIdMap.put(key, numberId); break; } } } return numberId; } private String getLevelText(ListStyleType listStyleType, int i) { if (listStyleType.getText() == null) { return ""; } if (listStyleType.getText().length() == 0) { return getOrderedLevelText(i); } return listStyleType.getText(); } /** * 获取有序列表项的序号格式 * * @param i 索引,从0开始 * @return 序号格式 */ private String getOrderedLevelText(int i) { return "%" + (i + 1) + "."; } private String getFormatKey() { StringBuilder sb = new StringBuilder(); for (ListStyle listStyle : listStyles) { sb.append(listStyle.getNumberFormat().getName()).append(StringUtils.SPACE); } return sb.toString(); } public void setIndent(int indent) { if (indent >= 0) { this.indent = indent; } } public void setHanging(int hanging) { if (hanging >= 0) { this.hanging = hanging; } } public void setSpacing(STLevelSuffix.Enum spacing) { this.spacing = spacing; } public boolean contains(XWPFParagraph paragraph) { return numberingParagraphs != null && numberingParagraphs.contains(paragraph); } } ================================================ FILE: src/main/java/org/ddr/poi/html/util/RenderUtils.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.html.util; import com.steadystate.css.dom.CSSStyleDeclarationImpl; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.math.NumberUtils; import org.apache.poi.util.Units; import org.apache.poi.xwpf.usermodel.BodyType; import org.apache.poi.xwpf.usermodel.IBody; import org.apache.poi.xwpf.usermodel.ParagraphAlignment; import org.apache.poi.xwpf.usermodel.TableRowAlign; import org.apache.poi.xwpf.usermodel.TableWidthType; import org.apache.poi.xwpf.usermodel.XWPFDocument; import org.apache.poi.xwpf.usermodel.XWPFParagraph; import org.apache.poi.xwpf.usermodel.XWPFTable; import org.apache.poi.xwpf.usermodel.XWPFTableCell; import org.ddr.poi.html.HtmlConstants; import org.ddr.poi.html.HtmlRenderContext; import org.openxmlformats.schemas.drawingml.x2006.main.CTPoint2D; import org.openxmlformats.schemas.drawingml.x2006.wordprocessingDrawing.CTAnchor; import org.openxmlformats.schemas.drawingml.x2006.wordprocessingDrawing.CTInline; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBorder; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTColor; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTDrawing; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTInd; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTJc; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTP; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPBdr; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPPr; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTRPr; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSectPr; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTShd; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSpacing; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTStyle; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTbl; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblBorders; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblPr; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblWidth; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTc; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcBorders; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcMar; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcPr; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTUnderline; import org.openxmlformats.schemas.wordprocessingml.x2006.main.STBorder; import org.openxmlformats.schemas.wordprocessingml.x2006.main.STLineSpacingRule; import org.openxmlformats.schemas.wordprocessingml.x2006.main.STTblWidth; import org.openxmlformats.schemas.wordprocessingml.x2006.main.STUnderline; import java.math.BigInteger; import java.util.function.Function; /** * 渲染相关的工具类 * * @author Draco * @since 2021-02-08 */ public class RenderUtils { /** * Word中字号下拉列表对应的值 */ public static final int[] FONT_SIZE_IN_HALF_POINTS = {10, 11, 13, 15, 18, 21, 24, 28, 30, 32, 36, 44, 48, 52, 72, 84, 96, 144}; /** * 边框宽度每像素对应值 */ public static final int BORDER_WIDTH_PER_PX = 4; /** * 最小边框宽度 */ public static final long MIN_BORDER_WIDTH = 2; /** * 最大边框宽度 */ public static final long MAX_BORDER_WIDTH = 96; /** * 表格单元格边距 */ public static final int TABLE_CELL_MARGIN = 108; /** * 段落行距系数 */ public static final int SPACING_FACTOR = 240; /** * 默认页面宽度 A4 portrait */ public static final int A4_WIDTH = 11906; /** * 默认页面高度 A4 portrait */ public static final int A4_HEIGHT = 16838; // Defined in [ Word 2010 look.dotx ] /** * 默认顶边距 */ public static final int DEFAULT_TOP_MARGIN = 1440; /** * 默认底边距 */ public static final int DEFAULT_BOTTOM_MARGIN = 1440; /** * 默认左边距 */ public static final int DEFAULT_LEFT_MARGIN = 1440; /** * 默认右边距 */ public static final int DEFAULT_RIGHT_MARGIN = 1440; /** * 文本对齐值映射 * * @param textAlign 文本对齐样式值 * @return Word文本对齐枚举 */ public static ParagraphAlignment align(String textAlign) { if (StringUtils.isBlank(textAlign)) { return null; } switch (textAlign.toLowerCase()) { case HtmlConstants.START: case HtmlConstants.LEFT: return ParagraphAlignment.LEFT; case HtmlConstants.END: case HtmlConstants.RIGHT: return ParagraphAlignment.RIGHT; case HtmlConstants.CENTER: return ParagraphAlignment.CENTER; case HtmlConstants.JUSTIFY: case HtmlConstants.JUSTIFY_ALL: return ParagraphAlignment.BOTH; default: return null; } } /** * 下划线样式映射 * * @param textDecorationStyle 下划线样式值 * @return Word下划线样式 */ public static STUnderline.Enum underline(String textDecorationStyle) { switch (textDecorationStyle) { // case HtmlConstants.SOLID: // return STUnderline.SINGLE; case HtmlConstants.DOUBLE: return STUnderline.DOUBLE; case HtmlConstants.DOTTED: return STUnderline.DOTTED; case HtmlConstants.DASHED: return STUnderline.DASH; case HtmlConstants.WAVY: return STUnderline.WAVE; default: return STUnderline.SINGLE; } } public static CTPPr getPPr(CTStyle ctStyle) { return ctStyle.isSetPPr() ? ctStyle.getPPr() : ctStyle.addNewPPr(); } public static CTPPr getPPr(CTP ctp) { return ctp.isSetPPr() ? ctp.getPPr() : ctp.addNewPPr(); } public static CTPBdr getPBdr(CTPPr pr) { return pr.isSetPBdr() ? pr.getPBdr() : pr.addNewPBdr(); } public static CTJc getJc(CTPPr pr) { return pr.isSetJc() ? pr.getJc() : pr.addNewJc(); } public static CTRPr getRPr(CTR ctr) { return ctr.isSetRPr() ? ctr.getRPr() : ctr.addNewRPr(); } public static CTTcPr getTcPr(CTTc tc) { return tc.isSetTcPr() ? tc.getTcPr() : tc.addNewTcPr(); } public static CTTcMar getTcMar(CTTcPr tcPr) { return tcPr.isSetTcMar() ? tcPr.getTcMar() : tcPr.addNewTcMar(); } public static CTTcMar getTcMar(XWPFTableCell cell) { CTTcPr tcPr = getTcPr(cell.getCTTc()); return getTcMar(tcPr); } public static CTShd getShd(CTPPr pPr) { return pPr.isSetShd() ? pPr.getShd() : pPr.addNewShd(); } public static CTInd getInd(CTPPr pPr) { return pPr.isSetInd() ? pPr.getInd() : pPr.addNewInd(); } public static CTInd getInd(XWPFParagraph paragraph) { CTPPr pPr = getPPr(paragraph.getCTP()); return getInd(pPr); } public static CTSpacing getSpacing(CTPPr pPr) { return pPr.isSetSpacing() ? pPr.getSpacing() : pPr.addNewSpacing(); } public static CTSpacing getSpacing(XWPFParagraph paragraph) { CTPPr pPr = getPPr(paragraph.getCTP()); return getSpacing(pPr); } public static CTColor getColor(CTRPr rPr) { return rPr.isSetColor() ? rPr.getColor() : rPr.addNewColor(); } public static CTUnderline getUnderline(CTRPr rPr) { return rPr.isSetU() ? rPr.getU() : rPr.addNewU(); } /** * 获取父容器的可用宽度,以EMU为单位 * * @param body 父容器 * @return 可用宽度 */ public static int getAvailableWidthInEMU(IBody body) { if (body.getPartType() == BodyType.DOCUMENT) { XWPFDocument document = (XWPFDocument) body; CTSectPr sectPr = document.getDocument().getBody().getSectPr(); int availableWidth = sectPr.getPgSz().getW().intValue() - sectPr.getPgMar().getLeft().intValue() - sectPr.getPgMar().getRight().intValue(); return Units.TwipsToEMU((short) availableWidth); } else if (body.getPartType() == BodyType.TABLECELL) { XWPFTableCell tableCell = ((XWPFTableCell) body); CTTblWidth tcW = tableCell.getCTTc().getTcPr().getTcW(); if (TableWidthType.DXA.getStWidthType().equals(tcW.getType())) { int availableWidth = tcW.getW().intValue() - TABLE_CELL_MARGIN * 2; return availableWidth > 0 ? Units.TwipsToEMU((short) availableWidth) : 0; } else if (TableWidthType.PCT.getStWidthType().equals(tcW.getType())) { CTTblWidth tblW = tableCell.getTableRow().getTable().getCTTbl().getTblPr().getTblW(); if (TableWidthType.DXA.getStWidthType().equals(tblW.getType())) { int availableWidth = tblW.getW().intValue() * tcW.getW().intValue() / 5000 - TABLE_CELL_MARGIN * 2; return availableWidth > 0 ? Units.TwipsToEMU((short) availableWidth) : 0; } else if (TableWidthType.NIL.getStWidthType().equals(tblW.getType())) { return 0; } else { return Integer.MAX_VALUE; } } else if (TableWidthType.NIL.getStWidthType().equals(tcW.getType())) { return 0; } else { return Integer.MAX_VALUE; } } else { throw new UnsupportedOperationException("Get bounds of " + body.getPartType() + " is not supported yet"); } } /** * 应用段落样式 * * @param context 渲染上下文 * @param paragraph 段落 * @param cssStyleDeclaration CSS样式声明 */ public static void paragraphStyle(HtmlRenderContext context, XWPFParagraph paragraph, CSSStyleDeclarationImpl cssStyleDeclaration) { /* inheritable styles */ // alignment ParagraphAlignment align = align(context.getPropertyValue(HtmlConstants.CSS_TEXT_ALIGN)); if (align != null) { paragraph.setAlignment(align); } if (CSSStyleUtils.isEmpty(cssStyleDeclaration)) { return; } // border setBorder(context, paragraph, cssStyleDeclaration); // spacing setSpacing(context, paragraph, cssStyleDeclaration); // indent setIndentation(context, paragraph, cssStyleDeclaration); // background String backgroundColor = cssStyleDeclaration.getBackgroundColor(); if (StringUtils.isNotBlank(backgroundColor)) { String color = Colors.fromStyle(backgroundColor, null); if (color != null) { CTPPr pPr = getPPr(paragraph.getCTP()); CTShd shd = getShd(pPr); shd.setFill(color); } } } /** * 设置段落行距 * * @param context 渲染上下文 * @param paragraph 段落 * @param cssStyleDeclaration CSS样式声明 */ private static void setSpacing(HtmlRenderContext context, XWPFParagraph paragraph, CSSStyleDeclarationImpl cssStyleDeclaration) { // margin-top CSSLength marginTop = CSSLength.of(cssStyleDeclaration.getMarginTop().toLowerCase()); if (marginTop.isValid() && !marginTop.isPercent()) { getSpacing(paragraph).setBefore(BigInteger.valueOf(emuToTwips(context.lengthToEMU(marginTop)))); } // margin-bottom CSSLength marginBottom = CSSLength.of(cssStyleDeclaration.getMarginBottom().toLowerCase()); if (marginBottom.isValid() && !marginBottom.isPercent()) { getSpacing(paragraph).setAfter(BigInteger.valueOf(emuToTwips(context.lengthToEMU(marginBottom)))); } // line-height String lineHeight = context.getPropertyValue(HtmlConstants.CSS_LINE_HEIGHT); if (StringUtils.isNotBlank(lineHeight)) { CSSLength cssLength = CSSLength.of(lineHeight); if (cssLength.isValid()) { if (cssLength.isPercent()) { CTSpacing spacing = getSpacing(paragraph); spacing.setLineRule(STLineSpacingRule.AUTO); spacing.setLine(BigInteger.valueOf(Math.round(cssLength.unitValue() * SPACING_FACTOR))); } else if (cssLength.getValue() > 0) { CTSpacing spacing = getSpacing(paragraph); spacing.setLineRule(STLineSpacingRule.EXACT); spacing.setLine(BigInteger.valueOf(emuToTwips(context.lengthToEMU(cssLength)))); } } else if (NumberUtils.isParsable(lineHeight)) { double value = Double.parseDouble(lineHeight); if (value > 0) { CTSpacing spacing = getSpacing(paragraph); spacing.setLineRule(STLineSpacingRule.AUTO); spacing.setLine(BigInteger.valueOf(Math.round(value * SPACING_FACTOR))); } } } } /** * 设置段落缩进 * * @param context 渲染上下文 * @param paragraph 段落 * @param cssStyleDeclaration CSS样式声明 */ private static void setIndentation(HtmlRenderContext context, XWPFParagraph paragraph, CSSStyleDeclarationImpl cssStyleDeclaration) { // margin-left CSSLength marginLeft = CSSLength.of(cssStyleDeclaration.getMarginLeft().toLowerCase()); if (marginLeft.isValid() && !marginLeft.isPercent()) { getInd(paragraph).setLeft(BigInteger.valueOf(emuToTwips(context.lengthToEMU(marginLeft)))); } // margin-right CSSLength marginRight = CSSLength.of(cssStyleDeclaration.getMarginRight().toLowerCase()); if (marginRight.isValid() && !marginRight.isPercent()) { getInd(paragraph).setRight(BigInteger.valueOf(emuToTwips(context.lengthToEMU(marginRight)))); } // text-indent String textIndent = context.getPropertyValue(HtmlConstants.CSS_TEXT_INDENT).trim(); if (textIndent.isEmpty()) { indent(context, paragraph, textIndent); } else { int nonSpace = -1; boolean indented = false; for (int i = 0; i < textIndent.length(); i++) { char c = textIndent.charAt(i); if (Character.isWhitespace(c)) { if (nonSpace >= 0) { indented = indent(context, paragraph, textIndent.substring(nonSpace, i)); } nonSpace = -1; } else if (nonSpace < 0) { nonSpace = i; } if (indented) { break; } } if (nonSpace >= 0) { indent(context, paragraph, textIndent.substring(nonSpace)); } } } /** * 段落首行缩进 * * @param context 渲染上下文 * @param paragraph 段落 * @param style 缩进样式值 * @return 是否进行了缩进 */ private static boolean indent(HtmlRenderContext context, XWPFParagraph paragraph, String style) { CSSLength cssLength = CSSLength.of(style.toLowerCase()); if (cssLength.isValid() && cssLength.getValue() > 0) { CTPPr pPr = getPPr(paragraph.getCTP()); CTInd ind = getInd(pPr); double indent; if (cssLength.isPercent()) { indent = context.getAvailableWidthInEMU() * cssLength.unitValue() / CSSLengthUnit.TWIP.absoluteFactor(); } else { indent = context.lengthToEMU(cssLength) / CSSLengthUnit.TWIP.absoluteFactor(); } ind.setFirstLine(BigInteger.valueOf(Math.round(indent))); return true; } return false; } /** * 设置段落边框样式 * * @param context 上下文 * @param xwpfElement 段落 * @param cssStyleDeclaration CSS边框样式声明 * @param styleProperty CSS边框属性名称 * @param widthProperty CSS边框宽度名称 * @param colorProperty CSS边框颜色名称 * @param getter 获取边框对象的方式 * @return 边框是否为none */ private static boolean setBorder(HtmlRenderContext context, Object xwpfElement, CSSStyleDeclarationImpl cssStyleDeclaration, String styleProperty, String widthProperty, String colorProperty, Function getter) { String borderStyle = cssStyleDeclaration.getPropertyValue(styleProperty); STBorder.Enum style = borderStyle(borderStyle); String borderWidth = cssStyleDeclaration.getPropertyValue(widthProperty); CSSLength width = CSSLength.of(borderWidth); if (style != null && (!width.isValid() || width.getValue() > 0)) { CTBorder border = getter.apply(xwpfElement); border.setVal(style); String borderColor = cssStyleDeclaration.getPropertyValue(colorProperty); String color = Colors.TRANSPARENT.equals(borderColor) ? Colors.WHITE : Colors.fromStyle(borderColor); border.setColor(color); if (width.isValid() && !width.isPercent()) { long widthValue = (long) context.lengthToEMU(width) * BORDER_WIDTH_PER_PX / Units.EMU_PER_PIXEL; if (widthValue < MIN_BORDER_WIDTH) { widthValue = MIN_BORDER_WIDTH; } else if (widthValue > MAX_BORDER_WIDTH) { widthValue = MAX_BORDER_WIDTH; } border.setSz(BigInteger.valueOf(widthValue)); } else { border.setSz(BigInteger.valueOf(BORDER_WIDTH_PER_PX)); } } return style == STBorder.NONE || style == STBorder.NIL; } /** * 边框样式映射 * * @param style CSS边框样式值 * @return Word边框样式 */ private static STBorder.Enum borderStyle(String style) { if (StringUtils.isBlank(style)) { return null; } switch (style.toLowerCase()) { case HtmlConstants.DOTTED: return STBorder.DOTTED; case HtmlConstants.DASHED: return STBorder.DASHED; case HtmlConstants.SOLID: return STBorder.SINGLE; case HtmlConstants.DOUBLE: return STBorder.DOUBLE; case HtmlConstants.GROOVE: case HtmlConstants.INSET: return STBorder.INSET; case HtmlConstants.RIDGE: case HtmlConstants.OUTSET: return STBorder.OUTSET; case HtmlConstants.NONE: return STBorder.NONE; default: return null; } } public static CTTblBorders getTblBorders(CTTbl tbl) { CTTblPr tblPr = getTblPr(tbl); return getTblBorders(tblPr); } private static CTBorder getTop(Object e) { if (e instanceof XWPFParagraph) { XWPFParagraph paragraph = (XWPFParagraph) e; return getParagraphTop(paragraph.getCTP()); } else if (e instanceof XWPFTable) { XWPFTable table = (XWPFTable) e; return getTableTop(table.getCTTbl()); } else if (e instanceof XWPFTableCell) { XWPFTableCell cell = (XWPFTableCell) e; return getTableCellTop(cell.getCTTc()); } else { throw new UnsupportedOperationException("Can not get top border of " + e.getClass().getName()); } } public static CTBorder getParagraphTop(CTP paragraph) { CTPPr pPr = getPPr(paragraph); CTPBdr pBdr = getPBdr(pPr); return pBdr.isSetTop() ? pBdr.getTop() : pBdr.addNewTop(); } public static CTBorder getTableTop(CTTbl table) { CTTblBorders tblBorders = getTblBorders(table); return tblBorders.isSetTop() ? tblBorders.getTop() : tblBorders.addNewTop(); } public static CTBorder getTableCellTop(CTTc cell) { CTTcPr tcPr = getTcPr(cell); CTTcBorders tcBorders = getTcBorders(tcPr); return tcBorders.isSetTop() ? tcBorders.getTop() : tcBorders.addNewTop(); } private static CTBorder getRight(Object e) { if (e instanceof XWPFParagraph) { XWPFParagraph paragraph = (XWPFParagraph) e; return getParagraphRight(paragraph.getCTP()); } else if (e instanceof XWPFTable) { XWPFTable table = (XWPFTable) e; return getTableRight(table.getCTTbl()); } else if (e instanceof XWPFTableCell) { XWPFTableCell cell = (XWPFTableCell) e; return getTableCellRight(cell.getCTTc()); } else { throw new UnsupportedOperationException("Can not get right border of " + e.getClass().getName()); } } public static CTBorder getParagraphRight(CTP paragraph) { CTPPr pPr = getPPr(paragraph); CTPBdr pBdr = getPBdr(pPr); return pBdr.isSetRight() ? pBdr.getRight() : pBdr.addNewRight(); } public static CTBorder getTableRight(CTTbl tbl) { CTTblBorders tblBorders = getTblBorders(tbl); return tblBorders.isSetRight() ? tblBorders.getRight() : tblBorders.addNewRight(); } public static CTBorder getTableCellRight(CTTc cell) { CTTcPr tcPr = getTcPr(cell); CTTcBorders tcBorders = getTcBorders(tcPr); return tcBorders.isSetRight() ? tcBorders.getRight() : tcBorders.addNewRight(); } private static CTBorder getBottom(Object e) { if (e instanceof XWPFParagraph) { XWPFParagraph paragraph = (XWPFParagraph) e; return getParagraphBottom(paragraph.getCTP()); } else if (e instanceof XWPFTable) { XWPFTable table = (XWPFTable) e; return getTableBottom(table.getCTTbl()); } else if (e instanceof XWPFTableCell) { XWPFTableCell cell = (XWPFTableCell) e; return getTableCellBottom(cell.getCTTc()); } else { throw new UnsupportedOperationException("Can not get bottom border of " + e.getClass().getName()); } } public static CTBorder getParagraphBottom(CTP paragraph) { CTPPr pPr = getPPr(paragraph); CTPBdr pBdr = getPBdr(pPr); return pBdr.isSetBottom() ? pBdr.getBottom() : pBdr.addNewBottom(); } public static CTBorder getTableBottom(CTTbl table) { CTTblBorders tblBorders = getTblBorders(table); return tblBorders.isSetBottom() ? tblBorders.getBottom() : tblBorders.addNewBottom(); } public static CTBorder getTableCellBottom(CTTc cell) { CTTcPr tcPr = getTcPr(cell); CTTcBorders tcBorders = getTcBorders(tcPr); return tcBorders.isSetBottom() ? tcBorders.getBottom() : tcBorders.addNewBottom(); } private static CTBorder getLeft(Object e) { if (e instanceof XWPFParagraph) { XWPFParagraph paragraph = (XWPFParagraph) e; return getParagraphLeft(paragraph.getCTP()); } else if (e instanceof XWPFTable) { XWPFTable table = (XWPFTable) e; return getTableLeft(table.getCTTbl()); } else if (e instanceof XWPFTableCell) { XWPFTableCell cell = (XWPFTableCell) e; return getTableCellLeft(cell.getCTTc()); } else { throw new UnsupportedOperationException("Can not get left border of " + e.getClass().getName()); } } public static CTBorder getParagraphLeft(CTP paragraph) { CTPPr pPr = getPPr(paragraph); CTPBdr pBdr = getPBdr(pPr); return pBdr.isSetLeft() ? pBdr.getLeft() : pBdr.addNewLeft(); } public static CTBorder getTableLeft(CTTbl table) { CTTblBorders tblBorders = getTblBorders(table); return tblBorders.isSetLeft() ? tblBorders.getLeft() : tblBorders.addNewLeft(); } public static CTBorder getTableCellLeft(CTTc cell) { CTTcPr tcPr = getTcPr(cell); CTTcBorders tcBorders = getTcBorders(tcPr); return tcBorders.isSetLeft() ? tcBorders.getLeft() : tcBorders.addNewLeft(); } /** * 获取小一号字号 * * @param inheritedSizeInHalfPoints 当前字号 * @return 字号 */ public static int smallerFontSizeInHalfPoints(int inheritedSizeInHalfPoints) { for (int i = FONT_SIZE_IN_HALF_POINTS.length - 1; i >= 0; i--) { int s = FONT_SIZE_IN_HALF_POINTS[i]; if (s < inheritedSizeInHalfPoints) { return s; } } return FONT_SIZE_IN_HALF_POINTS[0]; } /** * 获取大一号字号 * * @param inheritedSizeInHalfPoints 当前字号 * @return 字号 */ public static int largerFontSizeInHalfPoints(int inheritedSizeInHalfPoints) { for (int s : FONT_SIZE_IN_HALF_POINTS) { if (s > inheritedSizeInHalfPoints) { return s; } } return FONT_SIZE_IN_HALF_POINTS[FONT_SIZE_IN_HALF_POINTS.length - 1]; } /** * EMU转twip * * @see Units#TwipsToEMU */ public static int emuToTwips(int emu) { return (int) (emu * 20L / Units.EMU_PER_POINT); } /** * 应用表格样式 * * @param context 渲染上下文 * @param table 表格 * @param cssStyleDeclaration CSS样式声明 */ public static void tableStyle(HtmlRenderContext context, XWPFTable table, CSSStyleDeclarationImpl cssStyleDeclaration) { if (CSSStyleUtils.isEmpty(cssStyleDeclaration)) { return; } // alignment TableRowAlign align = alignTable(cssStyleDeclaration.getPropertyValue(HtmlConstants.CSS_FLOAT)); if (align != null) { table.setTableAlignment(align); } // border boolean allNone = setBorder(context, table, cssStyleDeclaration); // 如果四边都是none则将单元格间的边框也置为none if (allNone) { CTTblBorders tblBorders = getTblBorders(table.getCTTbl()); CTBorder insideH = getTblInsideH(tblBorders); insideH.setVal(STBorder.NONE); CTBorder insideV = getTblInsideV(tblBorders); insideV.setVal(STBorder.NONE); } // indent String marginLeft = cssStyleDeclaration.getPropertyValue(HtmlConstants.CSS_MARGIN_LEFT); if (StringUtils.isNotBlank(marginLeft)) { indent(context, table, marginLeft); } // background String backgroundColor = cssStyleDeclaration.getBackgroundColor(); if (StringUtils.isNotBlank(backgroundColor)) { String color = Colors.fromStyle(backgroundColor, null); if (color != null) { CTTblPr tblPr = getTblPr(table.getCTTbl()); CTShd shd = getShd(tblPr); shd.setFill(color); } } } public static CTBorder getTblInsideV(CTTblBorders tblBorders) { return tblBorders.isSetInsideV() ? tblBorders.getInsideV() : tblBorders.addNewInsideV(); } public static CTBorder getTblInsideH(CTTblBorders tblBorders) { return tblBorders.isSetInsideH() ? tblBorders.getInsideH() : tblBorders.addNewInsideH(); } /** * 应用表格样式 * * @param context 渲染上下文 * @param cell 表格 * @param cssStyleDeclaration CSS样式声明 */ public static void cellStyle(HtmlRenderContext context, XWPFTableCell cell, CSSStyleDeclarationImpl cssStyleDeclaration) { if (CSSStyleUtils.isEmpty(cssStyleDeclaration)) { return; } // padding setCellPadding(context, cell, cssStyleDeclaration); // alignment XWPFTableCell.XWPFVertAlign align = alignTableCell(cssStyleDeclaration.getVerticalAlign()); if (align != null) { cell.setVerticalAlignment(align); } // border setBorder(context, cell, cssStyleDeclaration); // background String backgroundColor = cssStyleDeclaration.getBackgroundColor(); if (StringUtils.isNotBlank(backgroundColor)) { String color = Colors.fromStyle(backgroundColor, null); if (color != null) { CTTcPr tcPr = getTcPr(cell.getCTTc()); CTShd shd = getShd(tcPr); shd.setFill(color); } } } /** * 设置单元格边距 * * @param context 渲染上下文 * @param cell 表格 * @param cssStyleDeclaration CSS样式声明 */ private static void setCellPadding(HtmlRenderContext context, XWPFTableCell cell, CSSStyleDeclarationImpl cssStyleDeclaration) { // margin-top CSSLength paddingTop = CSSLength.of(cssStyleDeclaration.getPaddingTop().toLowerCase()); if (paddingTop.isValid() && !paddingTop.isPercent() && paddingTop.getValue() >= 0) { CTTblWidth top = getTcMar(cell).addNewTop(); top.setType(STTblWidth.DXA); top.setW(BigInteger.valueOf(emuToTwips(context.lengthToEMU(paddingTop)))); } // margin-right CSSLength paddingRight = CSSLength.of(cssStyleDeclaration.getPaddingRight().toLowerCase()); if (paddingRight.isValid() && !paddingRight.isPercent() && paddingRight.getValue() >= 0) { CTTblWidth right = getTcMar(cell).addNewRight(); right.setType(STTblWidth.DXA); right.setW(BigInteger.valueOf(emuToTwips(context.lengthToEMU(paddingRight)))); } // margin-bottom CSSLength paddingBottom = CSSLength.of(cssStyleDeclaration.getPaddingBottom().toLowerCase()); if (paddingBottom.isValid() && !paddingBottom.isPercent() && paddingBottom.getValue() >= 0) { CTTblWidth bottom = getTcMar(cell).addNewBottom(); bottom.setType(STTblWidth.DXA); bottom.setW(BigInteger.valueOf(emuToTwips(context.lengthToEMU(paddingBottom)))); } // margin-left CSSLength paddingLeft = CSSLength.of(cssStyleDeclaration.getPaddingLeft().toLowerCase()); if (paddingLeft.isValid() && !paddingLeft.isPercent() && paddingLeft.getValue() >= 0) { CTTblWidth left = getTcMar(cell).addNewLeft(); left.setType(STTblWidth.DXA); left.setW(BigInteger.valueOf(emuToTwips(context.lengthToEMU(paddingLeft)))); } } /** * 设置上下左右边框样式 * * @param xwpfElement 元素 * @param cssStyleDeclaration CSS样式声明 * @return 是否四边全部为none */ public static boolean setBorder(HtmlRenderContext context, Object xwpfElement, CSSStyleDeclarationImpl cssStyleDeclaration) { boolean topNone = setBorder(context, xwpfElement, cssStyleDeclaration, HtmlConstants.CSS_BORDER_TOP_STYLE, HtmlConstants.CSS_BORDER_TOP_WIDTH, HtmlConstants.CSS_BORDER_TOP_COLOR, RenderUtils::getTop); boolean rightNone = setBorder(context, xwpfElement, cssStyleDeclaration, HtmlConstants.CSS_BORDER_RIGHT_STYLE, HtmlConstants.CSS_BORDER_RIGHT_WIDTH, HtmlConstants.CSS_BORDER_RIGHT_COLOR, RenderUtils::getRight); boolean bottomNone = setBorder(context, xwpfElement, cssStyleDeclaration, HtmlConstants.CSS_BORDER_BOTTOM_STYLE, HtmlConstants.CSS_BORDER_BOTTOM_WIDTH, HtmlConstants.CSS_BORDER_BOTTOM_COLOR, RenderUtils::getBottom); boolean leftNone = setBorder(context, xwpfElement, cssStyleDeclaration, HtmlConstants.CSS_BORDER_LEFT_STYLE, HtmlConstants.CSS_BORDER_LEFT_WIDTH, HtmlConstants.CSS_BORDER_LEFT_COLOR, RenderUtils::getLeft); return topNone && rightNone && bottomNone && leftNone; } private static boolean indent(HtmlRenderContext context, XWPFTable table, String style) { CSSLength cssLength = CSSLength.of(style.toLowerCase()); if (cssLength.isValid() && cssLength.getValue() > 0) { CTTblPr tblPr = getTblPr(table.getCTTbl()); CTTblWidth ind = getInd(tblPr); double indent; if (cssLength.isPercent()) { indent = context.getAvailableWidthInEMU() * cssLength.unitValue() / CSSLengthUnit.TWIP.absoluteFactor(); } else { indent = context.lengthToEMU(cssLength) / CSSLengthUnit.TWIP.absoluteFactor(); } ind.setType(STTblWidth.DXA); ind.setW(BigInteger.valueOf(Math.round(indent))); return true; } return false; } public static CTTblWidth getInd(CTTblPr tblPr) { return tblPr.isSetTblInd() ? tblPr.getTblInd() : tblPr.addNewTblInd(); } public static CTTblBorders getTblBorders(CTTblPr tblPr) { return tblPr.isSetTblBorders() ? tblPr.getTblBorders() : tblPr.addNewTblBorders(); } public static CTShd getShd(CTTblPr tblPr) { return tblPr.isSetShd() ? tblPr.getShd() : tblPr.addNewShd(); } public static CTTblPr getTblPr(CTTbl ctTbl) { CTTblPr tblPr = ctTbl.getTblPr(); if (tblPr == null) { tblPr = ctTbl.addNewTblPr(); } return tblPr; } public static CTTcBorders getTcBorders(CTTcPr tcPr) { return tcPr.isSetTcBorders() ? tcPr.getTcBorders() : tcPr.addNewTcBorders(); } public static CTShd getShd(CTTcPr tcPr) { return tcPr.isSetShd() ? tcPr.getShd() : tcPr.addNewShd(); } /** * 表格对齐值映射 * * @param cssFloat 表格对齐样式值 * @return Word表格对齐枚举 */ public static TableRowAlign alignTable(String cssFloat) { if (StringUtils.isBlank(cssFloat)) { return null; } switch (cssFloat.toLowerCase()) { case HtmlConstants.LEFT: return TableRowAlign.LEFT; case HtmlConstants.RIGHT: return TableRowAlign.RIGHT; default: return null; } } /** * 表格单元格垂直对齐值映射 * * @param verticalAlign 垂直对齐值 * @return Word表格单元格垂直对齐枚举 */ public static XWPFTableCell.XWPFVertAlign alignTableCell(String verticalAlign) { if (StringUtils.isBlank(verticalAlign)) { return null; } switch (verticalAlign.toLowerCase()) { case HtmlConstants.MIDDLE: return XWPFTableCell.XWPFVertAlign.CENTER; case HtmlConstants.BOTTOM: return XWPFTableCell.XWPFVertAlign.BOTTOM; default: return XWPFTableCell.XWPFVertAlign.TOP; } } /** * 嵌入式图片转换为环绕式图片 * * @param drawing 绘图容器 * @return 环绕式图片 */ public static CTAnchor inlineToAnchor(CTDrawing drawing) { CTInline ctInline = drawing.getInlineArray(0); CTAnchor ctAnchor = drawing.addNewAnchor(); ctAnchor.setDocPr(ctInline.getDocPr()); ctAnchor.setExtent(ctInline.getExtent()); ctAnchor.setGraphic(ctInline.getGraphic()); if (ctInline.isSetCNvGraphicFramePr()) { ctAnchor.setCNvGraphicFramePr(ctInline.getCNvGraphicFramePr()); } drawing.removeInline(0); ctAnchor.setAllowOverlap(false); ctAnchor.setBehindDoc(false); ctAnchor.setRelativeHeight(0); ctAnchor.setDistL(0); ctAnchor.setDistR(0); ctAnchor.setDistB(0); ctAnchor.setDistT(0); ctAnchor.setLayoutInCell(true); ctAnchor.setLocked(false); ctAnchor.setSimplePos2(false); CTPoint2D simplePos = ctAnchor.addNewSimplePos(); simplePos.setX(0); simplePos.setY(0); return ctAnchor; } } ================================================ FILE: src/main/java/org/ddr/poi/html/util/Span.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.html.util; import com.steadystate.css.dom.CSSStyleDeclarationImpl; /** * 记录表格单元格跨行列的帮助类 * * @author Draco * @since 2021-01-26 */ public class Span { private int row; private int column; private boolean enabled; private CSSStyleDeclarationImpl style; public Span(int row, int column, boolean enabled, CSSStyleDeclarationImpl style) { this.row = row; this.column = column; this.enabled = enabled; this.style = style; } public int getRow() { return this.row; } public int getColumn() { return this.column; } public boolean isEnabled() { return this.enabled; } public void setRow(int row) { this.row = row; } public void setColumn(int column) { this.column = column; } public void setEnabled(boolean enabled) { this.enabled = enabled; } public CSSStyleDeclarationImpl getStyle() { return style; } public String toString() { return "Span(row=" + this.getRow() + ", column=" + this.getColumn() + ", enabled=" + this.isEnabled() + ")"; } } ================================================ FILE: src/main/java/org/ddr/poi/html/util/SpanWidth.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.html.util; import java.util.Map; import java.util.Objects; /** * colspan对应的CSS长度值 * * @author Draco * @since 2021-10-19 */ public class SpanWidth extends CSSLength { private final int span; private final int column; private final CSSLength[] lengths; private final boolean explicitWidth; public SpanWidth(CSSLength length, int column, int span, boolean explicitWidth) { super(length.getValue(), length.getUnit()); this.column = column; this.span = span; lengths = new CSSLength[span]; this.explicitWidth = explicitWidth; } public void setLength(Map map) { if (!isValid()) { for (int i = 0; i < span; i++) { map.putIfAbsent(column + i, CSSLength.INVALID); } return; } int invalidCount = 0; boolean percent = isPercent(); double total = percent ? getValue() : unitValue(); CSSLengthUnit unit = percent ? CSSLengthUnit.PERCENT : CSSLengthUnit.EMU; for (int i = 0; i < span; i++) { Integer index = column + i; CSSLength length = map.getOrDefault(index, CSSLength.INVALID); lengths[i] = length; if (length.isValid()) { if (percent && length.isPercent()) { total -= length.getValue(); } else if (!percent && !length.isPercent()) { total -= length.unitValue(); } else { lengths[i] = CSSLength.INVALID; invalidCount++; } } else { invalidCount++; } } if (invalidCount > 0) { final CSSLength length = new CSSLength(total / invalidCount, unit); for (int i = 0; i < span; i++) { if (!lengths[i].isValid()) { map.compute(column + i, (key, value) -> { if (value == null || !value.isValid()) { return length; } else if (explicitWidth ^ percent) { return length; } return value; }); } } } } public int getSpan() { return span; } public int getColumn() { return column; } public CSSLength[] getLengths() { return lengths; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; if (!super.equals(o)) return false; SpanWidth spanWidth = (SpanWidth) o; return span == spanWidth.span && column == spanWidth.column && super.equals(o); } @Override public int hashCode() { return Objects.hash(column, span); } } ================================================ FILE: src/main/java/org/ddr/poi/html/util/WhiteSpaceRule.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.html.util; import org.ddr.poi.html.HtmlConstants; import java.util.Arrays; import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; /** * https://developer.mozilla.org/zh-CN/docs/Web/CSS/white-space * * @author Draco * @since 2021-07-15 */ public enum WhiteSpaceRule { NORMAL(HtmlConstants.NORMAL, false, false, false), NO_WRAP(HtmlConstants.NO_WRAP, false, false, false), PRE(HtmlConstants.PRE, true, true, true), PRE_WRAP(HtmlConstants.PRE_WRAP, true, true, true), PRE_LINE(HtmlConstants.PRE_LINE, true, false, false), BREAK_SPACES(HtmlConstants.BREAK_SPACES, true, true, true); private final String value; private final boolean keepLineBreak; private final boolean keepSpaceAndTab; private final boolean keepTrailingSpace; WhiteSpaceRule(String value, boolean keepLineBreak, boolean keepSpaceAndTab, boolean keepTrailingSpace) { this.value = value; this.keepLineBreak = keepLineBreak; this.keepSpaceAndTab = keepSpaceAndTab; this.keepTrailingSpace = keepTrailingSpace; } public String getValue() { return value; } public boolean isKeepLineBreak() { return keepLineBreak; } public boolean isKeepSpaceAndTab() { return keepSpaceAndTab; } public boolean isKeepTrailingSpace() { return keepTrailingSpace; } public boolean isNormal() { return this == NORMAL || this == NO_WRAP; } private static Map rules = Arrays.stream(values()).collect( Collectors.toMap(WhiteSpaceRule::getValue, Function.identity()) ); public static WhiteSpaceRule of(String value) { return rules.get(value); } public static WhiteSpaceRule of(String value, WhiteSpaceRule defaultRule) { return rules.getOrDefault(value, defaultRule); } } ================================================ FILE: src/main/java/org/ddr/poi/html/util/XWPFParagraphRuns.java ================================================ package org.ddr.poi.html.util; import org.apache.poi.xwpf.usermodel.IRunElement; import org.apache.poi.xwpf.usermodel.XWPFParagraph; import org.apache.poi.xwpf.usermodel.XWPFRun; import java.lang.reflect.Field; import java.util.List; /** * Wrapper class for XWPFParagraph runs * * @author Draco * @since 2022-07-05 */ public class XWPFParagraphRuns { private static Field runsField; private static Field irunsField; static { try { runsField = XWPFParagraph.class.getDeclaredField("runs"); irunsField = XWPFParagraph.class.getDeclaredField("iruns"); } catch (NoSuchFieldException ignored) { } runsField.setAccessible(true); irunsField.setAccessible(true); } private List runs; private List iruns; @SuppressWarnings("unchecked") public XWPFParagraphRuns(XWPFParagraph paragraph) { try { runs = (List) runsField.get(paragraph); iruns = (List) irunsField.get(paragraph); } catch (IllegalAccessException e) { throw new RuntimeException(e); } } /** * Remove run at position without modifying xml * * @param pos index of run */ public void remove(int pos) { XWPFRun run = runs.remove(pos); iruns.remove(run); } /** * @return runs count */ public int runCount() { return runs.size(); } } ================================================ FILE: src/main/java/org/ddr/poi/latex/LaTeXRenderPolicy.java ================================================ package org.ddr.poi.latex; import com.deepoove.poi.policy.AbstractRenderPolicy; import com.deepoove.poi.render.RenderContext; import org.apache.commons.lang3.StringUtils; import org.apache.poi.xwpf.usermodel.XWPFParagraph; import org.ddr.poi.math.MathRenderConfig; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR; import uk.ac.ed.ph.snuggletex.SnuggleSession; /** * LaTeX字符串渲染策略 * * @author Draco * @since 2021-04-14 */ public class LaTeXRenderPolicy extends AbstractRenderPolicy { private final MathRenderConfig config; private SnuggleSession session; public LaTeXRenderPolicy() { this(new MathRenderConfig()); } public LaTeXRenderPolicy(MathRenderConfig config) { this.config = config; } public MathRenderConfig getConfig() { return config; } @Override protected boolean validate(String data) { if (StringUtils.isBlank(data)) { return false; } // https://www2.ph.ed.ac.uk/snuggletex/documentation/overview-and-features.html session = LaTeXUtils.createSession(); return LaTeXUtils.parse(session, data); } @Override public void doRender(RenderContext context) throws Exception { XWPFParagraph paragraph = (XWPFParagraph) context.getRun().getParent(); CTR ctr = context.getRun().getCTR(); LaTeXUtils.renderTo(paragraph, ctr, session, config); } @Override protected void afterRender(RenderContext context) { clearPlaceholder(context, false); } } ================================================ FILE: src/main/java/org/ddr/poi/latex/LaTeXUtils.java ================================================ package org.ddr.poi.latex; import org.apache.commons.collections4.CollectionUtils; import org.apache.poi.xwpf.usermodel.TableWidthType; import org.apache.poi.xwpf.usermodel.XWPFParagraph; import org.apache.poi.xwpf.usermodel.XWPFTable; import org.apache.poi.xwpf.usermodel.XWPFTableCell; import org.apache.poi.xwpf.usermodel.XWPFTableRow; import org.apache.xmlbeans.XmlCursor; import org.apache.xmlbeans.XmlObject; import org.ddr.poi.html.util.Colors; import org.ddr.poi.math.MathMLUtils; import org.ddr.poi.math.MathRenderConfig; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBookmark; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPPr; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.dom.Text; import uk.ac.ed.ph.snuggletex.InputError; import uk.ac.ed.ph.snuggletex.SnuggleEngine; import uk.ac.ed.ph.snuggletex.SnuggleInput; import uk.ac.ed.ph.snuggletex.SnuggleSession; import uk.ac.ed.ph.snuggletex.definitions.CorePackageDefinitions; import uk.ac.ed.ph.snuggletex.definitions.Globals; import uk.ac.ed.ph.snuggletex.definitions.LaTeXMode; import uk.ac.ed.ph.snuggletex.dombuilding.MathComplexCommandHandler; import uk.ac.ed.ph.snuggletex.internal.util.XMLUtilities; import uk.ac.ed.ph.snuggletex.utilities.DefaultTransformerFactoryChooser; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.util.concurrent.ConcurrentHashMap; /** * LaTeX工具类 * * @author Draco * @since 2023-07-17 */ public class LaTeXUtils { private static final Logger log = LoggerFactory.getLogger(LaTeXUtils.class); public static final String TAG_MATH = "math"; public static final String TAG_TAG = "tag"; static final ConcurrentHashMap textCircledMap = new ConcurrentHashMap<>(); /** * 创建Snuggle会话 */ public static SnuggleSession createSession() { return Initializer.SNUGGLE_ENGINE.createSession(); } /** * 解析字符串 * * @param session Snuggle会话 * @param data LaTeX字符串 * @return 是否为有效的内容 */ public static boolean parse(SnuggleSession session, String data) { SnuggleInput input = new SnuggleInput(data); boolean valid = false; try { valid = session.parseInput(input); } catch (IOException ignored) { // Will never throw an exception since input is raw string } if (CollectionUtils.isNotEmpty(session.getErrors())) { log.warn("Invalid LaTex: {}", data); for (InputError error : session.getErrors()) { log.warn("LaTeX parse error: {}", error); } } return valid; } /** * 将LaTeX渲染到段落中 * * @param paragraph 段落 * @param ctr 目标run,如果总是在末尾渲染可传null * @param session Snuggle会话 * @param config 公式渲染配置 */ public static void renderTo(XWPFParagraph paragraph, CTR ctr, SnuggleSession session, MathRenderConfig config) { CTR target = ctr; NodeList nodeList = session.buildDOMSubtree(); int length = nodeList.getLength(); for (int i = 0; i < length; i++) { Node node = nodeList.item(i); if (node instanceof Text) { target = paragraph.getCTP().addNewR(); target.addNewT().setStringValue(node.getTextContent()); } else if (TAG_MATH.equals(node.getLocalName())) { String math = XMLUtilities.serializeNode(node, Initializer.SNUGGLE_ENGINE.getDefaultXMLStringOutputOptions()); MathMLUtils.renderTo(paragraph, target, math, config); } else if (TAG_TAG.equals(node.getLocalName())) { renderTag(paragraph, ctr, node, config); } } } private static void renderTag(XWPFParagraph paragraph, CTR ctr, Node node, MathRenderConfig config) { XmlCursor pCursor = paragraph.getCTP().newCursor(); pCursor.push(); XWPFTable xwpfTable = paragraph.getBody().insertNewTbl(pCursor); // 100% width xwpfTable.setWidth(5000); xwpfTable.setWidthType(TableWidthType.PCT); // no borders xwpfTable.setLeftBorder(XWPFTable.XWPFBorderType.NONE, 0, 0, Colors.BLACK); xwpfTable.setTopBorder(XWPFTable.XWPFBorderType.NONE, 0, 0, Colors.BLACK); xwpfTable.setRightBorder(XWPFTable.XWPFBorderType.NONE, 0, 0, Colors.BLACK); xwpfTable.setBottomBorder(XWPFTable.XWPFBorderType.NONE, 0, 0, Colors.BLACK); xwpfTable.setInsideHBorder(XWPFTable.XWPFBorderType.NONE, 0, 0, Colors.BLACK); xwpfTable.setInsideVBorder(XWPFTable.XWPFBorderType.NONE, 0, 0, Colors.BLACK); // access to the first row of the new table which created in a table cell will lead to a weird exception xwpfTable.removeRow(0); XWPFTableRow row = xwpfTable.createRow(); XWPFTableCell mathCell = row.createCell(); XWPFParagraph mathParagraph = mathCell.getParagraphs().get(0); XmlCursor copyCursor = mathParagraph.getCTP().newCursor(); copyCursor.toEndToken(); pCursor.pop(); pCursor.toFirstChild(); boolean hasNextSibling = true; while (hasNextSibling) { XmlObject obj = pCursor.getObject(); if (obj instanceof CTPPr) { pCursor.copyXml(copyCursor); hasNextSibling = pCursor.toNextSibling(); } else if (obj == null || obj instanceof CTBookmark || ctr.equals(obj)) { hasNextSibling = pCursor.toNextSibling(); } else { // moveXml附带了toNextSibling的效果 hasNextSibling = pCursor.moveXml(copyCursor); } } copyCursor.dispose(); pCursor.dispose(); // render math String math = XMLUtilities.serializeNode(node.getFirstChild(), Initializer.SNUGGLE_ENGINE.getDefaultXMLStringOutputOptions()); MathMLUtils.renderTo(mathParagraph, mathParagraph.createRun().getCTR(), math, config); // render tag XWPFTableCell tagCell = row.createCell(); tagCell.setVerticalAlignment(XWPFTableCell.XWPFVertAlign.CENTER); XWPFParagraph tagParagraph = tagCell.getParagraphs().get(0); String tag = XMLUtilities.serializeNode(node.getLastChild(), Initializer.SNUGGLE_ENGINE.getDefaultXMLStringOutputOptions()); MathMLUtils.renderTo(tagParagraph, tagParagraph.createRun().getCTR(), tag, config); } private static class Initializer { static final SnuggleEngine SNUGGLE_ENGINE = new SnuggleEngine(DefaultTransformerFactoryChooser.getInstance(), null); static { CorePackageDefinitions.getPackage().loadMathCharacterAliases("math-character-aliases.txt"); CorePackageDefinitions.getPackage().addComplexCommandSameArgMode("dfrac", false, 2, Globals.MATH_MODE_ONLY, new MathComplexCommandHandler("mfrac"), null); try (InputStream is = Globals.class.getClassLoader().getResourceAsStream("math-character-circled.txt"); InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8); BufferedReader br = new BufferedReader(isr)) { String line; while ((line = br.readLine()) != null) { if (line.startsWith("#")) { continue; } line = line.replaceFirst("\\s+#.+$", ""); String[] fields = line.split("->"); textCircledMap.put(fields[0], fields[1].trim()); } } catch (IOException e) { log.warn("Failed to load math-character-circled.txt", e); } CorePackageDefinitions.getPackage().addComplexCommandOneArg("textcircled", false, Globals.ALL_MODES, LaTeXMode.LR, new TextCircledHandler(), null); CorePackageDefinitions.getPackage().addComplexCommandOneArg("tag", false, Globals.ALL_MODES, LaTeXMode.LR, new TagHandler(), null); } } } ================================================ FILE: src/main/java/org/ddr/poi/latex/TagHandler.java ================================================ package org.ddr.poi.latex; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import uk.ac.ed.ph.snuggletex.definitions.CorePackageDefinitions; import uk.ac.ed.ph.snuggletex.definitions.W3CConstants; import uk.ac.ed.ph.snuggletex.dombuilding.CommandHandler; import uk.ac.ed.ph.snuggletex.internal.DOMBuilder; import uk.ac.ed.ph.snuggletex.internal.SnuggleParseException; import uk.ac.ed.ph.snuggletex.tokens.ArgumentContainerToken; import uk.ac.ed.ph.snuggletex.tokens.CommandToken; import uk.ac.ed.ph.snuggletex.tokens.FlowToken; import uk.ac.ed.ph.snuggletex.tokens.MathCharacterToken; import java.util.List; public class TagHandler implements CommandHandler { @Override public void handleCommand(DOMBuilder builder, Element parentElement, CommandToken token) throws SnuggleParseException { Document document = builder.getDocument(); Element table = document.createElementNS(W3CConstants.XHTML_NAMESPACE, LaTeXUtils.TAG_TAG); Node math; if (builder.isBuildingMathMLIsland()) { math = parentElement; while (math != null) { if (LaTeXUtils.TAG_MATH.equals(math.getLocalName()) && builder.getDocument().getDocumentElement().equals(math.getParentNode())) { break; } math = math.getParentNode(); } } else { math = parentElement.getLastChild(); while (math != null) { if (LaTeXUtils.TAG_MATH.equals(math.getLocalName())) { break; } math = math.getPreviousSibling(); } } if (math != null) { Node parentNode = math.getParentNode(); parentNode.removeChild(math); parentNode.appendChild(table); table.appendChild(math); ArgumentContainerToken argument = token.getArguments()[0]; List contents = argument.getContents(); contents.add(0, new MathCharacterToken(token.getSlice(), CorePackageDefinitions.getPackage().getMathCharacter("(".codePointAt(0)))); contents.add(new MathCharacterToken(token.getSlice(), CorePackageDefinitions.getPackage().getMathCharacter(")".codePointAt(0)))); builder.buildMathElement(table, token, argument, true); } } } ================================================ FILE: src/main/java/org/ddr/poi/latex/TextCircledHandler.java ================================================ package org.ddr.poi.latex; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Element; import uk.ac.ed.ph.snuggletex.dombuilding.CommandHandler; import uk.ac.ed.ph.snuggletex.internal.DOMBuilder; import uk.ac.ed.ph.snuggletex.internal.SnuggleParseException; import uk.ac.ed.ph.snuggletex.tokens.CommandToken; public class TextCircledHandler implements CommandHandler { private static final Logger log = LoggerFactory.getLogger(TextCircledHandler.class); @Override public void handleCommand(DOMBuilder builder, Element parentElement, CommandToken token) throws SnuggleParseException { String s = builder.extractStringValue(token.getArguments()[0]); String replacement = LaTeXUtils.textCircledMap.get(s); if (replacement != null) { if (builder.isBuildingMathMLIsland()) { builder.appendMathMLTextElement(parentElement, "mi", replacement, false); } else { builder.appendXHTMLTextElement(parentElement, "span", replacement, false); } } else { log.warn("Text circled not found: {}", s); } } } ================================================ FILE: src/main/java/org/ddr/poi/math/EmptyEOfNaryDisplayMode.java ================================================ package org.ddr.poi.math; /** * 空的N元组显示模式 * * @author Draco * @since 2025-06-09 16:44 */ public enum EmptyEOfNaryDisplayMode { /** * 默认:显示输入框 */ DEFAULT(0x00), /** * 零宽度 */ ZERO_WIDTH(0x01), /** * 隐藏输入框 */ HIDDEN(0x10), /** * 零宽度并且隐藏 */ ZERO_WIDTH_HIDDEN(0x11); private final int value; EmptyEOfNaryDisplayMode(int value) { this.value = value; } public int getValue() { return value; } public boolean isZeroWidth() { return (value & ZERO_WIDTH.getValue()) != 0; } public boolean isHidden() { return (value & HIDDEN.getValue()) != 0; } } ================================================ FILE: src/main/java/org/ddr/poi/math/MathMLRenderPolicy.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.math; import com.deepoove.poi.policy.AbstractRenderPolicy; import com.deepoove.poi.render.RenderContext; import org.apache.commons.lang3.StringUtils; import org.apache.poi.xwpf.usermodel.XWPFParagraph; import org.jsoup.Jsoup; import org.jsoup.nodes.Element; /** * MathML字符串渲染策略 * * @author Draco * @since 2021-04-10 22:51 */ public class MathMLRenderPolicy extends AbstractRenderPolicy { private final MathRenderConfig config; private Element math; public MathMLRenderPolicy() { this(new MathRenderConfig()); } public MathMLRenderPolicy(MathRenderConfig config) { this.config = config; } public MathRenderConfig getConfig() { return config; } @Override protected boolean validate(String data) { if (StringUtils.isBlank(data)) { return false; } math = Jsoup.parseBodyFragment(data).selectFirst("math"); return math != null; } @Override public void doRender(RenderContext context) throws Exception { if (!math.hasAttr("xmlns")) { math.attr("xmlns", "http://www.w3.org/1998/Math/MathML"); } String mathml = math.outerHtml(); MathMLUtils.renderTo((XWPFParagraph) context.getRun().getParent(), context.getRun().getCTR(), mathml, config); } @Override protected void afterRender(RenderContext context) { clearPlaceholder(context, false); } } ================================================ FILE: src/main/java/org/ddr/poi/math/MathMLUtils.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.math; import net.sf.saxon.s9api.Processor; import net.sf.saxon.s9api.SaxonApiException; import net.sf.saxon.s9api.Serializer; import net.sf.saxon.s9api.Xslt30Transformer; import net.sf.saxon.s9api.XsltCompiler; import net.sf.saxon.s9api.XsltExecutable; import org.apache.commons.lang3.StringEscapeUtils; import org.apache.poi.xwpf.usermodel.XWPFParagraph; import org.apache.xmlbeans.XmlCursor; import org.apache.xmlbeans.XmlException; import org.ddr.poi.util.XmlUtils; import org.openxmlformats.schemas.officeDocument.x2006.math.CTOMath; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTFonts; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTP; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.xml.namespace.QName; import javax.xml.transform.stream.StreamSource; import java.io.IOException; import java.io.InputStream; import java.io.StringReader; import java.io.StringWriter; import java.util.Arrays; import java.util.HashSet; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * MathML工具类 * * @author Draco * @since 2021-04-10 22:56 */ public class MathMLUtils { private static final Logger log = LoggerFactory.getLogger(MathMLUtils.class); private static final String MATH_FONT = "Cambria Math"; public static final String MATH_NS = "http://schemas.openxmlformats.org/officeDocument/2006/math"; public static final QName OMATH_QNAME = new QName(MATH_NS, "oMath"); public static final QName NARY_QNAME = new QName(MATH_NS, "nary"); public static final QName E_QNAME = new QName(MATH_NS, "e"); public static final QName PHANT_QNAME = new QName(MATH_NS, "phant"); public static final QName PHANT_PR_QNAME = new QName(MATH_NS, "phantPr"); public static final QName ZERO_WIDTH_QNAME = new QName(MATH_NS, "zeroWid"); public static final QName SHOW_QNAME = new QName(MATH_NS, "show"); public static final QName VALUE_QNAME = new QName(MATH_NS, "val"); private static final Pattern ESCAPED = Pattern.compile("&[a-zA-Z]+;"); private static final Set PREDEFINED = new HashSet<>(Arrays.asList("&", "<", ">", """, "'")); /** * 将MathML渲染到段落中 * * @param paragraph 段落 * @param ctr 占位符所属run,如果总是在末尾渲染可传null * @param math MathML字符串 */ public static void renderTo(XWPFParagraph paragraph, CTR ctr, String math, MathRenderConfig config) { if (log.isDebugEnabled()) { log.info("Start rendering MathML: {}", math); } try (StringReader sr = new StringReader(math); StringWriter sw = new StringWriter()) { Serializer out = newSerializer(sw); Initializer.TRANSFORMER.transform(new StreamSource(sr), out); String omath = sw.toString(); if (log.isDebugEnabled()) { log.info("Output OMath: {}", omath); } addMath(paragraph, ctr, omath, config); } catch (IOException | SaxonApiException | XmlException e) { log.warn("Failed to render math: {}", math, e); } } /** * 将html实体符号转换为xml形式 */ public static String normalize(String math) { Matcher matcher = ESCAPED.matcher(math); StringBuffer buffer = new StringBuffer(); while (matcher.find()) { String entity = matcher.group(); if (PREDEFINED.contains(entity)) { continue; } int codePoint = StringEscapeUtils.unescapeHtml4(entity).codePointAt(0); matcher.appendReplacement(buffer, "&#" + codePoint + ";"); } matcher.appendTail(buffer); math = buffer.toString(); return math; } /** * 用于惰性加载XSL转换器 */ private static class Initializer { static final Processor PROCESSOR = new Processor(false); static final Xslt30Transformer TRANSFORMER = createTransformer(); private static Xslt30Transformer createTransformer() { XsltCompiler compiler = PROCESSOR.newXsltCompiler(); XsltExecutable stylesheet; try (InputStream inputStream = MathMLUtils.class.getResourceAsStream("/MML2OMML.XSL")) { stylesheet = compiler.compile(new StreamSource(inputStream)); } catch (IOException | SaxonApiException e) { throw new IllegalStateException("Failed to load MML2OMML.XSL", e); } return stylesheet.load30(); } } private static Serializer newSerializer(StringWriter sw) { Serializer out = Initializer.PROCESSOR.newSerializer(sw); out.setOutputProperty(Serializer.Property.METHOD, "xml"); out.setOutputProperty(Serializer.Property.INDENT, "no"); out.setOutputProperty(Serializer.Property.OMIT_XML_DECLARATION, "yes"); return out; } /** * 添加公式到Word * * @param paragraph 段落 * @param ctr 占位符所属run,如果总是在末尾渲染可传null * @param omath 由mathml转换得到的omath字符串 */ private static void addMath(XWPFParagraph paragraph, CTR ctr, String omath, MathRenderConfig config) throws XmlException { CTOMath ctoMath = CTOMath.Factory.parse(omath); // 老版本Office可能无法正常显示,强制设置公式字体 XmlCursor xmlCursor = ctoMath.newCursor(); while (xmlCursor.hasNextToken()) { XmlCursor.TokenType tokenType = xmlCursor.toNextToken(); if (tokenType == XmlCursor.TokenType.START) { QName qName = xmlCursor.getName(); if (XmlUtils.R_QNAME.equals(qName)) { CTFonts ctFonts = ((org.openxmlformats.schemas.officeDocument.x2006.math.CTR) xmlCursor.getObject()).addNewRPr2().addNewRFonts(); ctFonts.setAscii(MATH_FONT); ctFonts.setHAnsi(MATH_FONT); } else if (E_QNAME.equals(qName)) { if (!xmlCursor.toFirstChild()) { xmlCursor.push(); xmlCursor.toParent(); QName parentName = xmlCursor.getName(); xmlCursor.pop(); if (NARY_QNAME.equals(parentName)) { if (config.getEmptyEOfNaryOption() != EmptyEOfNaryDisplayMode.DEFAULT) { xmlCursor.toNextToken(); xmlCursor.beginElement(PHANT_QNAME); xmlCursor.beginElement(PHANT_PR_QNAME); if (config.getEmptyEOfNaryOption().isZeroWidth()) { xmlCursor.beginElement(ZERO_WIDTH_QNAME); xmlCursor.toParent(); } if (config.getEmptyEOfNaryOption().isHidden()) { xmlCursor.beginElement(SHOW_QNAME); xmlCursor.insertAttributeWithValue(VALUE_QNAME, "off"); } } } } } } } xmlCursor.dispose(); CTP ctp = paragraph.getCTP(); if (ctr == null) { ctp.addNewOMath(); ctp.setOMathArray(ctp.sizeOfOMathArray() - 1, ctoMath.getOMathArray(0)); return; } insertMathAfterRun(ctp, ctr, ctoMath); } private static void insertMathAfterRun(CTP ctp, CTR ctr, CTOMath ctoMath) { XmlCursor xmlCursor; int oMathIndex = 0; boolean foundCTR = false; xmlCursor = ctp.newCursor(); while (xmlCursor.hasNextToken()) { XmlCursor.TokenType tokenType = xmlCursor.toNextToken(); if (tokenType == XmlCursor.TokenType.START) { if (xmlCursor.getObject() == ctr) { foundCTR = true; xmlCursor.toEndToken(); xmlCursor.toNextToken(); xmlCursor.insertElement(OMATH_QNAME); break; } else if (OMATH_QNAME.equals(xmlCursor.getName())) { oMathIndex++; } } } xmlCursor.dispose(); if (!foundCTR) { throw new IllegalArgumentException("The run does not belong to the paragraph"); } ctp.setOMathArray(oMathIndex, ctoMath.getOMathArray(0)); } } ================================================ FILE: src/main/java/org/ddr/poi/math/MathRenderConfig.java ================================================ package org.ddr.poi.math; /** * 公式渲染配置 * @author Draco * @since 2025-06-09 16:08 */ public class MathRenderConfig { private EmptyEOfNaryDisplayMode emptyEOfNaryDisplayMode = EmptyEOfNaryDisplayMode.DEFAULT; public EmptyEOfNaryDisplayMode getEmptyEOfNaryOption() { return emptyEOfNaryDisplayMode; } public void setEmptyEOfNaryOption(EmptyEOfNaryDisplayMode emptyEOfNaryDisplayMode) { this.emptyEOfNaryDisplayMode = emptyEOfNaryDisplayMode; } } ================================================ FILE: src/main/java/org/ddr/poi/util/ByteArrayCopyStream.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.util; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; /** * 可以复用输出的二进制数据并转换为输入的流 * * @author Draco * @since 2021-02-09 */ public class ByteArrayCopyStream extends ByteArrayOutputStream { public ByteArrayCopyStream() { } public ByteArrayCopyStream(int size) { super(size); } /** * @return 转换为输入流 */ public ByteArrayInputStream toInput() { return new ByteArrayInputStream(buf, 0, count); } } ================================================ FILE: src/main/java/org/ddr/poi/util/HttpURLConnectionUtils.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.util; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.StringUtils; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.ProtocolException; import java.net.URL; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.SecureRandom; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.Base64; /** * HttpURLConnection工具类,信任所有https地址 * * @author Draco * @since 2019-12-12 */ public class HttpURLConnectionUtils { public static final byte[] newLineBytes = "\r\n".getBytes(); public static class X509TrustAllManager implements X509TrustManager { @Override public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { } @Override public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { } @Override public X509Certificate[] getAcceptedIssuers() { return null; } } public static class TrustAllHostname implements HostnameVerifier { @Override public boolean verify(String s, SSLSession sslSession) { return true; } } /** * 开启http连接,默认未开启doOutput * * @param urlSpec url * @return http连接 */ public static HttpURLConnection connect(String urlSpec) throws IOException { return connect(urlSpec, null, null); } /** * 开启http连接,默认未开启doOutput * * @param urlSpec url * @param user basic auth用户名 * @param password basic auth密码 * @return http连接 */ public static HttpURLConnection connect(String urlSpec, String user, String password) throws IOException { if (!StringUtils.startsWith(urlSpec, "http")) { throw new IllegalArgumentException("Illegal url: " + urlSpec); } URL url = new URL(urlSpec); HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection(); httpURLConnection.setUseCaches(false); boolean isHttps = StringUtils.startsWith(urlSpec, "https"); if (isHttps) { HttpsURLConnection httpsURLConnection = (HttpsURLConnection) httpURLConnection; httpsURLConnection.setSSLSocketFactory(trustAllSslSocketFactory()); httpsURLConnection.setHostnameVerifier(new TrustAllHostname()); } if (user != null) { if (password == null) { password = ""; } String credential = user + ":" + password; String auth = "Basic " + Base64.getEncoder().encodeToString(credential.getBytes()); httpURLConnection.setRequestProperty("Authorization", auth); } return httpURLConnection; } public static SSLSocketFactory trustAllSslSocketFactory() { try { TrustManager[] trustManagers = {new X509TrustAllManager()}; SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE"); sslContext.init(null, trustManagers, SecureRandom.getInstance("SHA1PRNG")); return sslContext.getSocketFactory(); } catch (NoSuchAlgorithmException | NoSuchProviderException | KeyManagementException e) { throw new RuntimeException(e); } } /** * 初始化multipart/form-data请求头 * * @param connect http连接 * @return boundary */ public static String initFormData(HttpURLConnection connect) throws ProtocolException { connect.setDoOutput(true); connect.setRequestMethod("POST"); String boundary = "----WebKitFormBoundary" + RandomStringUtils.randomAlphanumeric(16); connect.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary); return boundary; } /** * 初始化User-Agent请求头 */ public static void initUserAgent(HttpURLConnection connect) { connect.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"); } /** * 添加FormData * @param field 字段名 * @param value 字段值 * @param data 附加数据 */ public static void addFormData(OutputStream outputStream, byte[] boundaryBytes, String field, String value, InputStream data) throws IOException { outputStream.write(boundaryBytes); outputStream.write(newLineBytes); outputStream.write("Content-Disposition: form-data; name=\"".getBytes()); outputStream.write(field.getBytes()); outputStream.write("\"".getBytes()); if (data != null) { outputStream.write("; filename=\"".getBytes()); outputStream.write(value.getBytes()); outputStream.write("\"".getBytes()); outputStream.write(newLineBytes); outputStream.write("Content-Type: application/octet-stream".getBytes()); outputStream.write(newLineBytes); outputStream.write(newLineBytes); // data byte[] buffer = new byte[8192]; int n; while (-1 != (n = data.read(buffer))) { outputStream.write(buffer, 0, n); } } else { outputStream.write(newLineBytes); outputStream.write(newLineBytes); outputStream.write(value.getBytes()); } outputStream.write(newLineBytes); } } ================================================ FILE: src/main/java/org/ddr/poi/util/XmlUtils.java ================================================ /* * Copyright 2016 - 2022 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.util; import org.apache.xmlbeans.XmlCursor; import org.apache.xmlbeans.XmlObject; import javax.xml.namespace.QName; import java.util.HashSet; import java.util.Set; /** * @author Draco * @since 2022-02-20 19:15 */ public class XmlUtils { public static final String NS_WORDPROCESSINGML = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"; public static final QName P_QNAME = new QName(NS_WORDPROCESSINGML, "p"); public static final QName PPR_QNAME = new QName(NS_WORDPROCESSINGML, "pPr"); public static final QName R_QNAME = new QName(NS_WORDPROCESSINGML, "r"); public static final QName BR_QNAME = new QName(NS_WORDPROCESSINGML, "br"); public static final QName TBL_QNAME = new QName(NS_WORDPROCESSINGML, "tbl"); public static final QName HYPERLINK_QNAME = new QName(NS_WORDPROCESSINGML, "hyperlink"); public static final QName BOOKMARK_START_QNAME = new QName(NS_WORDPROCESSINGML, "bookmarkStart"); public static final QName BOOKMARK_END_QNAME = new QName(NS_WORDPROCESSINGML, "bookmarkEnd"); public static final Set INVALID_R_SIBLINGS = new HashSet<>(); static { INVALID_R_SIBLINGS.add(PPR_QNAME); INVALID_R_SIBLINGS.add(BOOKMARK_START_QNAME); INVALID_R_SIBLINGS.add(BOOKMARK_END_QNAME); } /** * 移除xml元素上声明的命名空间 * * @param xmlObject xml元素 */ public static void removeNamespaces(XmlObject xmlObject) { XmlCursor cursor = xmlObject.newCursor(); cursor.toNextToken(); while (cursor.hasNextToken()) { if (cursor.isNamespace()) { cursor.removeXml(); } else { cursor.toNextToken(); } } cursor.dispose(); } } ================================================ FILE: src/main/java/org/jsoup/parser/CustomHtmlTreeBuilder.java ================================================ package org.jsoup.parser; import org.ddr.poi.html.HtmlConstants; import org.jsoup.nodes.Element; import org.jsoup.nodes.Node; import java.util.List; /** * 自定义html树构建器,对于svg及其内部的标签使用xml解析模式 * * @author Draco * @since 2022-04-15 */ public class CustomHtmlTreeBuilder extends HtmlTreeBuilder { @Override void reconstructFormattingElements() { boolean settingsChanged = false; ParseSettings origin = settings; if (isSvgElement()) { settings = ParseSettings.preserveCase; settingsChanged = true; } super.reconstructFormattingElements(); if (settingsChanged) { settings = origin; } } @Override Element insert(Token.StartTag startTag) { boolean settingsChanged = false; ParseSettings origin = settings; if (isSvgElement()) { settings = ParseSettings.preserveCase; settingsChanged = true; } Element element = super.insert(startTag); if (settingsChanged) { settings = origin; } return element; } @Override public List parseFragment(String inputFragment, Element context, String baseUri, Parser parser) { return super.parseFragment(inputFragment, context, baseUri, parser); } private boolean isSvgElement() { if (currentToken.isStartTag() && HtmlConstants.TAG_SVG.equals(currentToken.asStartTag().normalName)) { return true; } return getFromStack(HtmlConstants.TAG_SVG) != null; } } ================================================ FILE: src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi ================================================ org.ddr.image.avif.AvifImageReaderSpi org.ddr.image.heif.HeifImageReaderSpi ================================================ FILE: src/main/resources/MML2OMML.XSL ================================================ ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz 1 0 1 0 1 0 1 0 1 0 p b script script b double-struck p fraktur p fraktur b sans-serif p sans-serif b sans-serif sans-serif bi b bi 1 1 0 noBar skw bar on 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 on on on on on on on on off 1 0 1 0 lin 1 0 1 0 1 0 1 0 0 1 0 1 0 0 1 0 1 0 1 0 0 bot top top ̆ ̒ ̀ ̅ ̅ ̇ ̇ ̋ ́ ̃ ̃ ̈ ̌ ̂ ̅ top bot bot " " 1 0 1 0 1 0 1 0 0 & 1 0 1 1 0 1 0 1 0 1 off on on on 1 0 1 0 undOvr subSup 1 0 1 0 on off on off ================================================ FILE: src/main/resources/math-character-aliases.txt ================================================ to->rightarrow gets->leftarrow implies->Longrightarrow ================================================ FILE: src/main/resources/math-character-circled.txt ================================================ 0->⓪ 1->① 2->② 3->③ 4->④ 5->⑤ 6->⑥ 7->⑦ 8->⑧ 9->⑨ 10->⑩ 11->⑪ 12->⑫ 13->⑬ 14->⑭ 15->⑮ 16->⑯ 17->⑰ 18->⑱ 19->⑲ 20->⑳ 21->㉑ 22->㉒ 23->㉓ 24->㉔ 25->㉕ 26->㉖ 27->㉗ 28->㉘ 29->㉙ 30->㉚ 31->㉛ 32->㉜ 33->㉝ 34->㉞ 35->㉟ 36->㊱ 37->㊲ 38->㊳ 39->㊴ 40->㊵ 41->㊶ 42->㊷ 43->㊸ 44->㊹ 45->㊺ 46->㊻ 47->㊼ 48->㊽ 49->㊾ 50->㊿ A->Ⓐ B->Ⓑ C->Ⓒ D->Ⓓ E->Ⓔ F->Ⓕ G->Ⓖ H->Ⓗ I->Ⓘ J->Ⓙ K->Ⓚ L->Ⓛ M->Ⓜ N->Ⓝ O->Ⓞ P->Ⓟ Q->Ⓠ R->Ⓡ S->Ⓢ T->Ⓣ U->Ⓤ V->Ⓥ W->Ⓦ X->Ⓧ Y->Ⓨ Z->Ⓩ a->ⓐ b->ⓑ c->ⓒ d->ⓓ e->ⓔ f->ⓕ g->ⓖ h->ⓗ i->ⓘ j->ⓙ k->ⓚ l->ⓛ m->ⓜ n->ⓝ o->ⓞ p->ⓟ q->ⓠ r->ⓡ s->ⓢ t->ⓣ u->ⓤ v->ⓥ w->ⓦ x->ⓧ y->ⓨ z->ⓩ 一->㊀ 二->㊁ 三->㊂ 四->㊃ 五->㊄ 六->㊅ 七->㊆ 八->㊇ 九->㊈ 十->㊉ ================================================ FILE: src/test/java/org/ddr/poi/FileReader.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi; import org.apache.commons.io.IOUtils; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; /** * @author Draco * @since 2021-04-12 21:20 */ public class FileReader { public static String readFile(String resourceFile) throws IOException { try (InputStream inputStream = FileReader.class.getResourceAsStream(resourceFile)) { return IOUtils.toString(inputStream, StandardCharsets.UTF_8); } } } ================================================ FILE: src/test/java/org/ddr/poi/html/HtmlRenderPolicyTest.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.html; import com.deepoove.poi.XWPFTemplate; import com.deepoove.poi.config.Configure; import org.ddr.poi.FileReader; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.Map; class HtmlRenderPolicyTest { @Test void doRender() throws IOException { HtmlRenderPolicy htmlRenderPolicy = new HtmlRenderPolicy(); Configure configure = Configure.builder() .bind("teachContent", htmlRenderPolicy) .bind("plainContent", htmlRenderPolicy) .build(); Map data = new HashMap<>(2); data.put("teachContent", FileReader.readFile("/1.html")); data.put("plainContent", FileReader.readFile("/2.html")); try (InputStream inputStream = HtmlRenderPolicyTest.class.getResourceAsStream("/notes.docx")) { XWPFTemplate.compile(inputStream, configure).render(data).writeToFile("notes_out.docx"); } // 段落内嵌入html测试 try (InputStream inputStream = HtmlRenderPolicyTest.class.getResourceAsStream("/poi.docx")) { XWPFTemplate.compile(inputStream, configure).render(data).writeToFile("poi_out.docx"); } } } ================================================ FILE: src/test/java/org/ddr/poi/latex/LaTeXRenderPolicyTest.java ================================================ package org.ddr.poi.latex; import com.deepoove.poi.XWPFTemplate; import com.deepoove.poi.config.Configure; import org.ddr.poi.math.EmptyEOfNaryDisplayMode; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.Map; class LaTeXRenderPolicyTest { @Test void doRender() throws IOException { LaTeXRenderPolicy laTeXRenderPolicy = new LaTeXRenderPolicy(); laTeXRenderPolicy.getConfig().setEmptyEOfNaryOption(EmptyEOfNaryDisplayMode.ZERO_WIDTH_HIDDEN); Configure configure = Configure.builder() .bind("math1", laTeXRenderPolicy) .bind("math2", laTeXRenderPolicy) .bind("math3", laTeXRenderPolicy) .bind("math4", laTeXRenderPolicy) .build(); Map data = new HashMap<>(4); // https://www2.ph.ed.ac.uk/snuggletex/documentation/math-mode.html data.put("math1", "$$ x+2=3 $$"); data.put("math2", "\\[ \\sum_{i=1}^{\\infty} \\frac{1}{n^s} \n" + "= \\prod_p \\frac{1}{1 - p^{-s}} \\tag{1.1}\\]"); data.put("math3", "Product $\\prod_{i=a}^{b} f(i) \\tag{\\textcircled{1}}$ inside text. $\\mathbb{N} \\mathbf{N} \\mathbb{Z} \\mathbf{Z} \\mathbb{D} \\mathbf{D} \\mathbb{Q} \\mathbf{Q} \\mathbb{R} \\mathbf{R} \\mathbb{C} \\mathbf{C}$"); data.put("math4", "$\\lim_{x\\to\\infty} f(x)$"); try (InputStream inputStream = LaTeXRenderPolicyTest.class.getResourceAsStream("/math.docx")) { XWPFTemplate.compile(inputStream, configure).render(data).writeToFile("latex_out.docx"); } } } ================================================ FILE: src/test/java/org/ddr/poi/math/MathMLRenderPolicyTest.java ================================================ /* * Copyright 2016 - 2021 Draco, https://github.com/draco1023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ddr.poi.math; import com.deepoove.poi.XWPFTemplate; import com.deepoove.poi.config.Configure; import org.ddr.poi.FileReader; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; /** * @author Draco * @since 2021-04-11 8:33 */ class MathMLRenderPolicyTest { @Test void doRender() throws IOException { MathMLRenderPolicy mathMLRenderPolicy = new MathMLRenderPolicy(); Configure configure = Configure.builder() .bind("math1", mathMLRenderPolicy) .bind("math2", mathMLRenderPolicy) .bind("math3", mathMLRenderPolicy) .bind("math4", mathMLRenderPolicy) .build(); List mathList = new ArrayList(4); for (int i = 0; i < 4; i++) { mathList.add(FileReader.readFile("/" + i + ".xml")); } Collections.shuffle(mathList); Map data = new HashMap<>(mathList.size()); for (int i = 0; i < mathList.size(); i++) { data.put("math" + (i + 1), mathList.get(i)); } try (InputStream inputStream = MathMLRenderPolicyTest.class.getResourceAsStream("/math.docx")) { XWPFTemplate.compile(inputStream, configure).render(data).writeToFile("math_out.docx"); } } } ================================================ FILE: src/test/resources/0.xml ================================================ ( a + b ) 2 = c 2 + 4 ( 1 2 a b ) a 2 + 2 a b + b 2 = c 2 + 2 a b a 2 + b 2 = c 2 ================================================ FILE: src/test/resources/1.html ================================================

你好世界敢信啊!

你好世界 你敢信啊!

================================================ FILE: src/test/resources/1.xml ================================================ x 2 + 4 x + 4 = 0 ================================================ FILE: src/test/resources/2.html ================================================

你好世界 你敢信啊!

你好世界 你敢信啊!

4   1 4

  围殴皮肤较为

威风威风文   coolhttp://www.baidu.com

  1. 违法未
  2. 违法未

今天(21日)0时,位于新疆南部的塔里木油田年油气产量达到3003.12万吨,成为我国油气上产重要战略接替区。

  位于大北博孜气区的大北902井近日喷出高产气流,今年这里试油的6口井均获40万立方米以上高产。目前,天山南麓还有60多部钻机正在钻进,近两年,塔里木油田超过100口气井获高产,新建天然气产能90亿立方米。

  
 

  中石油塔里木油田公司总经理 杨学文:“十三五”期间,塔里木油田大力提升勘探开发力度,已经落实两个万亿方大气区,形成10亿吨级大油气区,为新一轮的油气增储上产奠定了坚实的资源基础。

  56万平方公里的塔里木盆地是我国陆上最大的含油气盆地,盆地遍布大漠戈壁,油气大多蕴藏在超过6000米的地宫深处,面临超深、超高温、超高压等极限考验,是世界级勘探禁区。油田的科技工作者攻克了一系列世界级难题,探明油气储量26.7亿吨。




  • 威风威风
  • 违法未
    1. 测试嵌套123213和纳税空间圣诞节ask的风景啊的肌肤啊撒旦解放深刻搭街坊埃德加发生的纠纷啊深刻搭街坊啊撒旦看风景啊的肌肤阿斯达克警方啊撒旦解放啊可是大家发深刻搭街坊阿斯达克激发深刻搭街坊ask的风景阿斯达克放假啊上的飞机啊扣税的,上的飞机ask大家罚款。

    2. 测试嵌套2222
      1. 测试嵌套3333
      2. 测试嵌套123213和纳税空间圣诞节ask的风景啊的肌肤啊撒旦解放深刻搭街坊埃德加发生的纠纷啊深刻搭街坊啊撒旦看风景啊的肌肤阿斯达克警方啊撒旦解放啊可是大家发深刻搭街坊阿斯达克激发深刻搭街坊ask的风景阿斯达克放假啊上的飞机啊扣税的
  • 违法未
    1. 测试另一个嵌套123213
    2. 测试另一个嵌套2222

 

(导)学案

备 注

学习目标

 

 

The table header
The table body with two columns
The table header
The table body with two columns
ttttttt

test 

学习重点

 

 

 

知识链接

 

 

 

学法指导

 

 

 

学习过程

问题与任务

方法与要求

-

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

达标检测

课内作业

课外作业

-

 

 

 

 

 

总结提升

 

 

 

课后反思

 

 

 

test

创建和编辑页面是不能被 keep-alive 缓存的,因为keep-alive 的 include 目前不支持根据路由来缓存,所以目前都是基于 component name 来进行缓存的。如果你想类似的实现缓存效果,可以使用 localStorage 等浏览器缓存方案。或者不要使用 keep-alive 的 include,直接缓存所有页面。详情见 Document

               
士大夫            
  啊发撒地方              
    撒地方的撒          
               
               
               

java生成word的几种方案

方案描述表

方案描述表副标题

序号

荷载(kN)

历时(min)

沉降(mm)

本级1

累计1

本级2

累计2

111

222

333

444

555

666

最大沉降量:19.88mm    最大回弹量:8.66mm 回弹率:43.56%

111111111

11111
1111 123445

1、啊啊啊啊啊啊啊

2、呃呃呃呃呃呃呃

3、噢噢噢噢噢噢噢

111111 2 1
11111 疾控科考虑考虑萨芬框架啊沙发上
售后服务 (10.0分) 无售后服务
333 1111 111
11111 啥打发时间开了房卡拉斯科发链接
333333 33333333333 333333333333333333333

$$ x+2=3 $$Product $\prod_{i=a}^{b} f(i)$ inside text. $\mathbb{N} \mathbf{N} \mathbb{Z} \mathbf{Z} \mathbb{D} \mathbf{D} \mathbb{Q} \mathbf{Q} \mathbb{R} \mathbf{R} \mathbb{C} \mathbf{C}$

\[ \sum_{i=1}^{\infty} \frac{1}{n^s} = \prod_p \frac{1}{1 - p^{-s}} \]

================================================ FILE: src/test/resources/2.xml ================================================ 4 ================================================ FILE: src/test/resources/3.xml ================================================ π r 2 ================================================ FILE: src/test/resources/simplelogger.properties ================================================ # # Copyright 2016 - 2021 Draco, https://github.com/draco1023 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # org.slf4j.simpleLogger.logFile=System.out org.slf4j.simpleLogger.defaultLogLevel=debug org.slf4j.simpleLogger.showDateTime=true