**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
 
# Maven
poi 4.x poi-tl 1.11 以前的版本
```xml
io.github.draco1023poi-tl-ext0.4.26
```
poi 5.x poi-tl 1.11.0+
```xml
io.github.draco1023poi-tl-ext0.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`
## 支持我
如果您觉得这个插件节省了您的时间和精力,或者解决了您的难题,可以考虑支持一下我的工作,感谢! ⚡⚡⚡

================================================
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