From 64db32717b1a5d4bc616b4d7ab1b4fdb04e59dda Mon Sep 17 00:00:00 2001
From: chenzhangyue <15696756582@163.com>
Date: Fri, 21 Jun 2024 16:16:49 +0800
Subject: [PATCH 1/4] :bookmark: add new tools
---
pom.xml | 2 +-
.../java/com/luna/common/file/FileTools.java | 344 ++-
.../luna/common/img/BackgroundRemoval.java | 361 +++
.../java/com/luna/common/img/ColorUtil.java | 263 ++
.../java/com/luna/common/img/FontUtil.java | 106 +
.../com/luna/common/img/GraphicsUtil.java | 208 ++
src/main/java/com/luna/common/img/Img.java | 852 ++++++
.../java/com/luna/common/img/ImgUtil.java | 2469 +++++++++++++++++
.../java/com/luna/common/img/LabColor.java | 91 +
.../java/com/luna/common/img/ScaleType.java | 43 +
.../common/img/gif/AnimatedGifEncoder.java | 585 ++++
.../com/luna/common/img/gif/GifDecoder.java | 784 ++++++
.../com/luna/common/img/gif/LZWEncoder.java | 306 ++
.../com/luna/common/img/gif/NeuQuant.java | 460 +++
.../com/luna/common/img/gif/package-info.java | 7 +
.../com/luna/common/img/package-info.java | 7 +
.../luna/common/swing/ClipboardListener.java | 23 +
.../luna/common/swing/ClipboardMonitor.java | 217 ++
.../com/luna/common/swing/ClipboardUtil.java | 171 ++
.../com/luna/common/swing/DesktopUtil.java | 114 +
.../com/luna/common/swing/ImageSelection.java | 64 +
.../java/com/luna/common/swing/RobotUtil.java | 230 ++
.../com/luna/common/swing/ScreenUtil.java | 87 +
.../common/swing/StrClipboardListener.java | 34 +
.../com/luna/common/swing/package-info.java | 7 +
.../com/luna/common/utils/ObjectUtils.java | 43 +
26 files changed, 7856 insertions(+), 22 deletions(-)
create mode 100755 src/main/java/com/luna/common/img/BackgroundRemoval.java
create mode 100644 src/main/java/com/luna/common/img/ColorUtil.java
create mode 100755 src/main/java/com/luna/common/img/FontUtil.java
create mode 100755 src/main/java/com/luna/common/img/GraphicsUtil.java
create mode 100755 src/main/java/com/luna/common/img/Img.java
create mode 100755 src/main/java/com/luna/common/img/ImgUtil.java
create mode 100644 src/main/java/com/luna/common/img/LabColor.java
create mode 100755 src/main/java/com/luna/common/img/ScaleType.java
create mode 100755 src/main/java/com/luna/common/img/gif/AnimatedGifEncoder.java
create mode 100755 src/main/java/com/luna/common/img/gif/GifDecoder.java
create mode 100755 src/main/java/com/luna/common/img/gif/LZWEncoder.java
create mode 100755 src/main/java/com/luna/common/img/gif/NeuQuant.java
create mode 100755 src/main/java/com/luna/common/img/gif/package-info.java
create mode 100755 src/main/java/com/luna/common/img/package-info.java
create mode 100755 src/main/java/com/luna/common/swing/ClipboardListener.java
create mode 100755 src/main/java/com/luna/common/swing/ClipboardMonitor.java
create mode 100755 src/main/java/com/luna/common/swing/ClipboardUtil.java
create mode 100755 src/main/java/com/luna/common/swing/DesktopUtil.java
create mode 100755 src/main/java/com/luna/common/swing/ImageSelection.java
create mode 100755 src/main/java/com/luna/common/swing/RobotUtil.java
create mode 100755 src/main/java/com/luna/common/swing/ScreenUtil.java
create mode 100755 src/main/java/com/luna/common/swing/StrClipboardListener.java
create mode 100755 src/main/java/com/luna/common/swing/package-info.java
diff --git a/pom.xml b/pom.xml
index f84e4cbdf..1365ea195 100644
--- a/pom.xml
+++ b/pom.xml
@@ -7,7 +7,7 @@
+ * 此方法不对File对象类型做判断,如果File不存在,无法判断其类型
+ *
+ * @param path 相对ClassPath的目录或者绝对路径目录,使用POSIX风格
+ * @return 文件,若路径为null,返回null
+ * @throws RuntimeException IO异常
+ */
+ public static File touch(String path) throws RuntimeException {
+ if (path == null) {
+ return null;
+ }
+ return touch(file(path));
+ }
+
/**
* 创建文件及其父目录,如果这个文件存在,直接返回这个文件
* 此方法不对File对象类型做判断,如果File不存在,无法判断其类型
@@ -592,13 +611,13 @@ public static BufferedOutputStream getOutputStream(File file) {
* @return 文件,若路径为null,返回null
* @throws IOException IO异常
*/
- public static File touch(File file) throws IOException {
+ public static File touch(File file) {
if (null == file) {
return null;
}
if (!file.exists()) {
- Files.createFile(file.toPath());
try {
+ Files.createFile(file.toPath());
// noinspection ResultOfMethodCallIgnored
file.createNewFile();
} catch (Exception e) {
@@ -609,35 +628,32 @@ public static File touch(File file) throws IOException {
}
/**
- * 创建文件及其父目录,如果这个文件存在,直接返回这个文件
- * 此方法不对File对象类型做判断,如果File不存在,无法判断其类型
+ * 创建File对象,自动识别相对或绝对路径,相对路径将自动从ClassPath下寻找
*
- * @param path 相对ClassPath的目录或者绝对路径目录,使用POSIX风格
- * @return 文件,若路径为null,返回null
- * @throws IORuntimeException IO异常
+ * @param path 相对ClassPath的目录或者绝对路径目录
+ * @return File
*/
- public static File touch(String path) {
- if (path == null) {
+ public static File file(String path) {
+ if (null == path) {
return null;
}
- try {
- return touch(file(path));
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
+ return new File(path);
}
/**
- * 创建File对象,自动识别相对或绝对路径,相对路径将自动从ClassPath下寻找
+ * 创建File对象
+ * 根据的路径构建文件,在Win下直接构建,在Linux下拆分路径单独构建
+ * 此方法会检查slip漏洞,漏洞说明见http://blog.nsfocus.net/zip-slip-2/
*
- * @param path 相对ClassPath的目录或者绝对路径目录
+ * @param parent 父文件对象
+ * @param path 文件路径
* @return File
*/
- public static File file(String path) {
- if (null == path) {
- return null;
+ public static File file(File parent, String path) {
+ if (StringTools.isBlank(path)) {
+ throw new NullPointerException("File path is blank!");
}
- return new File(path);
+ return checkSlip(parent, buildFile(parent, path));
}
/**
@@ -672,9 +688,295 @@ public static BufferedWriter getWriter(File file, Charset charset, boolean isApp
* @param file 文件
* @param charset 字符集
* @return BufferedReader对象
- * @throws IORuntimeException IO异常
+ * @throws RuntimeException IO异常
*/
public static BufferedReader getReader(File file, Charset charset) {
return IoUtil.getReader(getInputStream(file), charset);
}
+
+ /**
+ * 检查两个文件是否是同一个文件
+ * 所谓文件相同,是指Path对象是否指向同一个文件或文件夹
+ *
+ * @param file1 文件1
+ * @param file2 文件2
+ * @return 是否相同
+ * @throws RuntimeException IO异常
+ * @see Files#isSameFile(Path, Path)
+ * @since 5.4.1
+ */
+ public static boolean equals(Path file1, Path file2) throws RuntimeException {
+ try {
+ return Files.isSameFile(file1, file2);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * 检查两个文件是否是同一个文件
+ * 所谓文件相同,是指File对象是否指向同一个文件或文件夹
+ *
+ * @param file1 文件1
+ * @param file2 文件2
+ * @return 是否相同
+ * @throws RuntimeException IO异常
+ */
+ public static boolean equals(File file1, File file2) throws RuntimeException {
+ Assert.notNull(file1);
+ Assert.notNull(file2);
+ if (false == file1.exists() || false == file2.exists()) {
+ // 两个文件都不存在判断其路径是否相同, 对于一个存在一个不存在的情况,一定不相同
+ return false == file1.exists()//
+ && false == file2.exists()//
+ && pathEquals(file1, file2);
+ }
+ return equals(file1.toPath(), file2.toPath());
+ }
+
+ /**
+ * 文件路径是否相同
+ * 取两个文件的绝对路径比较,在Windows下忽略大小写,在Linux下不忽略。
+ *
+ * @param file1 文件1
+ * @param file2 文件2
+ * @return 文件路径是否相同
+ * @since 3.0.9
+ */
+ public static boolean pathEquals(File file1, File file2) {
+ if (isWindows()) {
+ // Windows环境
+ try {
+ if (StringTools.equalsIgnoreCase(file1.getCanonicalPath(), file2.getCanonicalPath())) {
+ return true;
+ }
+ } catch (Exception e) {
+ if (StringTools.equalsIgnoreCase(file1.getAbsolutePath(), file2.getAbsolutePath())) {
+ return true;
+ }
+ }
+ } else {
+ // 类Unix环境
+ try {
+ if (StringTools.equals(file1.getCanonicalPath(), file2.getCanonicalPath())) {
+ return true;
+ }
+ } catch (Exception e) {
+ if (StringTools.equals(file1.getAbsolutePath(), file2.getAbsolutePath())) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 检查父完整路径是否为自路径的前半部分,如果不是说明不是子路径,可能存在slip注入。
+ *
+ * 见http://blog.nsfocus.net/zip-slip-2/
+ *
+ * @param parentFile 父文件或目录
+ * @param file 子文件或目录
+ * @return 子文件或目录
+ * @throws IllegalArgumentException 检查创建的子文件不在父目录中抛出此异常
+ */
+ public static File checkSlip(File parentFile, File file) throws IllegalArgumentException {
+ if (null != parentFile && null != file) {
+ if (!isSub(parentFile, file)) {
+ throw new IllegalArgumentException("New file is outside of the parent dir: " + file.getName());
+ }
+ }
+ return file;
+ }
+
+ /**
+ * 判断给定的目录是否为给定文件或文件夹的子目录
+ *
+ * @param parent 父目录
+ * @param sub 子目录
+ * @return 子目录是否为父目录的子目录
+ * @since 4.5.4
+ */
+ public static boolean isSub(File parent, File sub) {
+ Assert.notNull(parent);
+ Assert.notNull(sub);
+ return isSub(parent.toPath(), sub.toPath());
+ }
+
+ /**
+ * 判断给定的目录是否为给定文件或文件夹的子目录
+ *
+ * @param parent 父目录
+ * @param sub 子目录
+ * @return 子目录是否为父目录的子目录
+ * @since 5.5.5
+ */
+ public static boolean isSub(Path parent, Path sub) {
+ return PathUtil.toAbsNormal(sub).startsWith(PathUtil.toAbsNormal(parent));
+ }
+
+ /**
+ * 根据压缩包中的路径构建目录结构,在Win下直接构建,在Linux下拆分路径单独构建
+ *
+ * @param outFile 最外部路径
+ * @param fileName 文件名,可以包含路径
+ * @return 文件或目录
+ * @since 5.0.5
+ */
+ private static File buildFile(File outFile, String fileName) {
+ // 替换Windows路径分隔符为Linux路径分隔符,便于统一处理
+ fileName = fileName.replace('\\', '/');
+ if (false == isWindows()
+ // 检查文件名中是否包含"/",不考虑以"/"结尾的情况
+ && fileName.lastIndexOf(CharPoolConstant.SLASH, fileName.length() - 2) > 0) {
+ // 在Linux下多层目录创建存在问题,/会被当成文件名的一部分,此处做处理
+ // 使用/拆分路径(zip中无\),级联创建父目录
+ final List
+ * 图片背景识别处理、背景替换、背景设置为矢量图
+ *
+ * 根据一定规则算出图片背景色的RGB值,进行替换
+ *
+ * 2020-05-21 16:36
+ *
+ * 方法来自:com.lnwazg.kit
+ *
+ * @param colorName 颜色的英文名,16进制表示或RGB表示
+ * @return {@link Color}
+ * @since 4.1.14
+ */
+ public static Color getColor(String colorName) {
+ if (StringTools.isBlank(colorName)) {
+ return null;
+ }
+ colorName = colorName.toUpperCase();
+
+ if ("BLACK".equals(colorName)) {
+ return Color.BLACK;
+ } else if ("WHITE".equals(colorName)) {
+ return Color.WHITE;
+ } else if ("LIGHTGRAY".equals(colorName) || "LIGHT_GRAY".equals(colorName)) {
+ return Color.LIGHT_GRAY;
+ } else if ("GRAY".equals(colorName)) {
+ return Color.GRAY;
+ } else if ("DARKGRAY".equals(colorName) || "DARK_GRAY".equals(colorName)) {
+ return Color.DARK_GRAY;
+ } else if ("RED".equals(colorName)) {
+ return Color.RED;
+ } else if ("PINK".equals(colorName)) {
+ return Color.PINK;
+ } else if ("ORANGE".equals(colorName)) {
+ return Color.ORANGE;
+ } else if ("YELLOW".equals(colorName)) {
+ return Color.YELLOW;
+ } else if ("GREEN".equals(colorName)) {
+ return Color.GREEN;
+ } else if ("MAGENTA".equals(colorName)) {
+ return Color.MAGENTA;
+ } else if ("CYAN".equals(colorName)) {
+ return Color.CYAN;
+ } else if ("BLUE".equals(colorName)) {
+ return Color.BLUE;
+ } else if ("DARKGOLD".equals(colorName)) {
+ // 暗金色
+ return hexToColor("#9e7e67");
+ } else if ("LIGHTGOLD".equals(colorName)) {
+ // 亮金色
+ return hexToColor("#ac9c85");
+ } else if (StringTools.startWith(colorName, '#')) {
+ return hexToColor(colorName);
+ } else if (StringTools.startWith(colorName, '$')) {
+ // 由于#在URL传输中无法传输,因此用$代替#
+ return hexToColor("#" + colorName.substring(1));
+ } else {
+ // rgb值
+ final List
+ * 方法来自:com.lnwazg.kit
+ *
+ * @param colorName 颜色的英文名,16进制表示或RGB表示
+ * @return {@link Color}
+ * @see ColorUtil#getColor(String)
+ * @since 4.1.14
+ */
+ public static Color getColor(String colorName) {
+ return ColorUtil.getColor(colorName);
+ }
+
+ /**
+ * 生成随机颜色
+ *
+ * @return 随机颜色
+ * @see ColorUtil#randomColor()
+ * @since 3.1.2
+ */
+ public static Color randomColor() {
+ return ColorUtil.randomColor();
+ }
+
+ /**
+ * 生成随机颜色
+ *
+ * @param random 随机对象 {@link Random}
+ * @return 随机颜色
+ * @see ColorUtil#randomColor(Random)
+ * @since 3.1.2
+ */
+ public static Color randomColor(Random random) {
+ return ColorUtil.randomColor(random);
+ }
+
+ /**
+ * 获得修正后的矩形坐标位置,变为以背景中心为基准坐标(即x,y == 0,0时,处于背景正中)
+ *
+ * @param rectangle 矩形
+ * @param backgroundWidth 参考宽(背景宽)
+ * @param backgroundHeight 参考高(背景高)
+ * @return 修正后的{@link Point}
+ * @since 5.3.6
+ */
+ public static Point getPointBaseCentre(Rectangle rectangle, int backgroundWidth, int backgroundHeight) {
+ return new Point(
+ rectangle.x + (Math.abs(backgroundWidth - rectangle.width) / 2), //
+ rectangle.y + (Math.abs(backgroundHeight - rectangle.height) / 2)//
+ );
+ }
+
+ /**
+ * 获取给定图片的主色调,背景填充用
+ *
+ * @param image {@link BufferedImage}
+ * @param rgbFilters 过滤多种颜色
+ * @return {@link String} #ffffff
+ * @since 5.6.7
+ */
+ public static String getMainColor(BufferedImage image, int[]... rgbFilters) {
+ return ColorUtil.getMainColor(image, rgbFilters);
+ }
+ // ------------------------------------------------------------------------------------------------------ 背景图换算
+
+ /**
+ * 背景移除
+ * 图片去底工具
+ * 将 "纯色背景的图片" 还原成 "透明背景的图片"
+ * 将纯色背景的图片转成矢量图
+ * 取图片边缘的像素点和获取到的图片主题色作为要替换的背景色
+ * 再加入一定的容差值,然后将所有像素点与该颜色进行比较
+ * 发现相同则将颜色不透明度设置为0,使颜色完全透明.
+ *
+ * @param inputPath 要处理图片的路径
+ * @param outputPath 输出图片的路径
+ * @param tolerance 容差值[根据图片的主题色,加入容差值,值的范围在0~255之间]
+ * @return 返回处理结果 true:图片处理完成 false:图片处理失败
+ */
+ public static boolean backgroundRemoval(String inputPath, String outputPath, int tolerance) {
+ return BackgroundRemoval.backgroundRemoval(inputPath, outputPath, tolerance);
+ }
+
+ /**
+ * 背景移除
+ * 图片去底工具
+ * 将 "纯色背景的图片" 还原成 "透明背景的图片"
+ * 将纯色背景的图片转成矢量图
+ * 取图片边缘的像素点和获取到的图片主题色作为要替换的背景色
+ * 再加入一定的容差值,然后将所有像素点与该颜色进行比较
+ * 发现相同则将颜色不透明度设置为0,使颜色完全透明.
+ *
+ * @param input 需要进行操作的图片
+ * @param output 最后输出的文件
+ * @param tolerance 容差值[根据图片的主题色,加入容差值,值的取值范围在0~255之间]
+ * @return 返回处理结果 true:图片处理完成 false:图片处理失败
+ */
+ public static boolean backgroundRemoval(File input, File output, int tolerance) {
+ return BackgroundRemoval.backgroundRemoval(input, output, tolerance);
+ }
+
+ /**
+ * 背景移除
+ * 图片去底工具
+ * 将 "纯色背景的图片" 还原成 "透明背景的图片"
+ * 将纯色背景的图片转成矢量图
+ * 取图片边缘的像素点和获取到的图片主题色作为要替换的背景色
+ * 再加入一定的容差值,然后将所有像素点与该颜色进行比较
+ * 发现相同则将颜色不透明度设置为0,使颜色完全透明.
+ *
+ * @param input 需要进行操作的图片
+ * @param output 最后输出的文件
+ * @param override 指定替换成的背景颜色 为null时背景为透明
+ * @param tolerance 容差值[根据图片的主题色,加入容差值,值的取值范围在0~255之间]
+ * @return 返回处理结果 true:图片处理完成 false:图片处理失败
+ */
+ public static boolean backgroundRemoval(File input, File output, Color override, int tolerance) {
+ return BackgroundRemoval.backgroundRemoval(input, output, override, tolerance);
+ }
+
+ /**
+ * 背景移除
+ * 图片去底工具
+ * 将 "纯色背景的图片" 还原成 "透明背景的图片"
+ * 将纯色背景的图片转成矢量图
+ * 取图片边缘的像素点和获取到的图片主题色作为要替换的背景色
+ * 再加入一定的容差值,然后将所有像素点与该颜色进行比较
+ * 发现相同则将颜色不透明度设置为0,使颜色完全透明.
+ *
+ * @param bufferedImage 需要进行处理的图片流
+ * @param override 指定替换成的背景颜色 为null时背景为透明
+ * @param tolerance 容差值[根据图片的主题色,加入容差值,值的取值范围在0~255之间]
+ * @return 返回处理好的图片流
+ */
+ public static BufferedImage backgroundRemoval(BufferedImage bufferedImage, Color override, int tolerance) {
+ return BackgroundRemoval.backgroundRemoval(bufferedImage, override, tolerance);
+ }
+
+ /**
+ * 背景移除
+ * 图片去底工具
+ * 将 "纯色背景的图片" 还原成 "透明背景的图片"
+ * 将纯色背景的图片转成矢量图
+ * 取图片边缘的像素点和获取到的图片主题色作为要替换的背景色
+ * 再加入一定的容差值,然后将所有像素点与该颜色进行比较
+ * 发现相同则将颜色不透明度设置为0,使颜色完全透明.
+ *
+ * @param outputStream 需要进行处理的图片字节数组流
+ * @param override 指定替换成的背景颜色 为null时背景为透明
+ * @param tolerance 容差值[根据图片的主题色,加入容差值,值的取值范围在0~255之间]
+ * @return 返回处理好的图片流
+ */
+ public static BufferedImage backgroundRemoval(ByteArrayOutputStream outputStream, Color override, int tolerance) {
+ return BackgroundRemoval.backgroundRemoval(outputStream, override, tolerance);
+ }
+
+ /**
+ * 图片颜色转换
+ * Data URI的格式规范:
+ *
+ *
+ * 来自:https://github.com/rtyley/animated-gif-lib-for-java
+ *
+ * @author Kevin Weiner, FM Software
+ * @version 1.03 November 2003
+ * @since 5.3.8
+ */
+public class AnimatedGifEncoder {
+
+ protected int width; // image size
+ protected int height;
+ protected Color transparent = null; // transparent color if given
+ protected boolean transparentExactMatch = false; // transparent color will be found by looking for
+ // the closest color
+ // or for the exact color if transparentExactMatch == true
+ protected Color background = null; // background color if given
+ protected int transIndex; // transparent index in color table
+ protected int repeat = -1; // no repeat
+ protected int delay = 0; // frame delay (hundredths)
+ protected boolean started = false; // ready to output frames
+ protected OutputStream out;
+ protected BufferedImage image; // current frame
+ protected byte[] pixels; // BGR byte array from frame
+ protected byte[] indexedPixels; // converted frame indexed to palette
+ protected int colorDepth; // number of bit planes
+ protected byte[] colorTab; // RGB palette
+ protected boolean[] usedEntry = new boolean[256]; // active palette entries
+ protected int palSize = 7; // color table size (bits-1)
+ protected int dispose = -1; // disposal code (-1 = use default)
+ protected boolean closeStream = false; // close stream when finished
+ protected boolean firstFrame = true;
+ protected boolean sizeSet = false; // if false, get size from first frame
+ protected int sample = 10; // default sample interval for quantizer
+
+ /**
+ * 设置每一帧的间隔时间
+ * Sets the delay time between each frame, or changes it
+ * for subsequent frames (applies to last frame added).
+ *
+ * @param ms 间隔时间,单位毫秒
+ */
+ public void setDelay(int ms) {
+ delay = Math.round(ms / 10.0f);
+ }
+
+ /**
+ * Sets the GIF frame disposal code for the last added frame
+ * and any subsequent frames. Default is 0 if no transparent
+ * color has been set, otherwise 2.
+ *
+ * @param code int disposal code.
+ */
+ public void setDispose(int code) {
+ if (code >= 0) {
+ dispose = code;
+ }
+ }
+
+ /**
+ * Sets the number of times the set of GIF frames
+ * should be played. Default is 1; 0 means play
+ * indefinitely. Must be invoked before the first
+ * image is added.
+ *
+ * @param iter int number of iterations.
+ */
+ public void setRepeat(int iter) {
+ if (iter >= 0) {
+ repeat = iter;
+ }
+ }
+
+ /**
+ * Sets the transparent color for the last added frame
+ * and any subsequent frames.
+ * Since all colors are subject to modification
+ * in the quantization process, the color in the final
+ * palette for each frame closest to the given color
+ * becomes the transparent color for that frame.
+ * May be set to null to indicate no transparent color.
+ *
+ * @param c Color to be treated as transparent on display.
+ */
+ public void setTransparent(Color c) {
+ setTransparent(c, false);
+ }
+
+ /**
+ * Sets the transparent color for the last added frame
+ * and any subsequent frames.
+ * Since all colors are subject to modification
+ * in the quantization process, the color in the final
+ * palette for each frame closest to the given color
+ * becomes the transparent color for that frame.
+ * If exactMatch is set to true, transparent color index
+ * is search with exact match, and not looking for the
+ * closest one.
+ * May be set to null to indicate no transparent color.
+ *
+ * @param c Color to be treated as transparent on display.
+ * @param exactMatch If exactMatch is set to true, transparent color index is search with exact match
+ */
+ public void setTransparent(Color c, boolean exactMatch) {
+ transparent = c;
+ transparentExactMatch = exactMatch;
+ }
+
+ /**
+ * Sets the background color for the last added frame
+ * and any subsequent frames.
+ * Since all colors are subject to modification
+ * in the quantization process, the color in the final
+ * palette for each frame closest to the given color
+ * becomes the background color for that frame.
+ * May be set to null to indicate no background color
+ * which will default to black.
+ *
+ * @param c Color to be treated as background on display.
+ */
+ public void setBackground(Color c) {
+ background = c;
+ }
+
+ /**
+ * Adds next GIF frame. The frame is not written immediately, but is
+ * actually deferred until the next frame is received so that timing
+ * data can be inserted. Invoking {@code finish()} flushes all
+ * frames. If {@code setSize} was not invoked, the size of the
+ * first image is used for all subsequent frames.
+ *
+ * @param im BufferedImage containing frame to write.
+ * @return true if successful.
+ */
+ public boolean addFrame(BufferedImage im) {
+ if ((im == null) || !started) {
+ return false;
+ }
+ boolean ok = true;
+ try {
+ if (!sizeSet) {
+ // use first frame's size
+ setSize(im.getWidth(), im.getHeight());
+ }
+ image = im;
+ getImagePixels(); // convert to correct format if necessary
+ analyzePixels(); // build color table & map pixels
+ if (firstFrame) {
+ writeLSD(); // logical screen descriptior
+ writePalette(); // global color table
+ if (repeat >= 0) {
+ // use NS app extension to indicate reps
+ writeNetscapeExt();
+ }
+ }
+ writeGraphicCtrlExt(); // write graphic control extension
+ writeImageDesc(); // image descriptor
+ if (!firstFrame) {
+ writePalette(); // local color table
+ }
+ writePixels(); // encode and write pixel data
+ firstFrame = false;
+ } catch (IOException e) {
+ ok = false;
+ }
+
+ return ok;
+ }
+
+ /**
+ * Flushes any pending data and closes output file.
+ * If writing to an OutputStream, the stream is not
+ * closed.
+ *
+ * @return is ok
+ */
+ public boolean finish() {
+ if (!started)
+ return false;
+ boolean ok = true;
+ started = false;
+ try {
+ out.write(0x3b); // gif trailer
+ out.flush();
+ if (closeStream) {
+ out.close();
+ }
+ } catch (IOException e) {
+ ok = false;
+ }
+
+ // reset for subsequent use
+ transIndex = 0;
+ out = null;
+ image = null;
+ pixels = null;
+ indexedPixels = null;
+ colorTab = null;
+ closeStream = false;
+ firstFrame = true;
+
+ return ok;
+ }
+
+ /**
+ * Sets frame rate in frames per second. Equivalent to
+ * {@code setDelay(1000/fps)}.
+ *
+ * @param fps float frame rate (frames per second)
+ */
+ public void setFrameRate(float fps) {
+ if (fps != 0f) {
+ delay = Math.round(100f / fps);
+ }
+ }
+
+ /**
+ * Sets quality of color quantization (conversion of images
+ * to the maximum 256 colors allowed by the GIF specification).
+ * Lower values (minimum = 1) produce better colors, but slow
+ * processing significantly. 10 is the default, and produces
+ * good color mapping at reasonable speeds. Values greater
+ * than 20 do not yield significant improvements in speed.
+ *
+ * @param quality int greater than 0.
+ */
+ public void setQuality(int quality) {
+ if (quality < 1)
+ quality = 1;
+ sample = quality;
+ }
+
+ /**
+ * Sets the GIF frame size. The default size is the
+ * size of the first frame added if this method is
+ * not invoked.
+ *
+ * @param w int frame width.
+ * @param h int frame width.
+ */
+ public void setSize(int w, int h) {
+ if (started && !firstFrame)
+ return;
+ width = w;
+ height = h;
+ if (width < 1)
+ width = 320;
+ if (height < 1)
+ height = 240;
+ sizeSet = true;
+ }
+
+ /**
+ * Initiates GIF file creation on the given stream. The stream
+ * is not closed automatically.
+ *
+ * @param os OutputStream on which GIF images are written.
+ * @return false if initial write failed.
+ */
+ public boolean start(OutputStream os) {
+ if (os == null)
+ return false;
+ boolean ok = true;
+ closeStream = false;
+ out = os;
+ try {
+ writeString("GIF89a"); // header
+ } catch (IOException e) {
+ ok = false;
+ }
+ return started = ok;
+ }
+
+ /**
+ * Initiates writing of a GIF file with the specified name.
+ *
+ * @param file String containing output file name.
+ * @return false if open or initial write failed.
+ */
+ public boolean start(String file) {
+ boolean ok;
+ try {
+ out = new BufferedOutputStream(Files.newOutputStream(Paths.get(file)));
+ ok = start(out);
+ closeStream = true;
+ } catch (IOException e) {
+ ok = false;
+ }
+ return started = ok;
+ }
+
+ public boolean isStarted() {
+ return started;
+ }
+
+ /**
+ * Analyzes image colors and creates color map.
+ */
+ protected void analyzePixels() {
+ int len = pixels.length;
+ int nPix = len / 3;
+ indexedPixels = new byte[nPix];
+ NeuQuant nq = new NeuQuant(pixels, len, sample);
+ // initialize quantizer
+ colorTab = nq.process(); // create reduced palette
+ // convert map from BGR to RGB
+ for (int i = 0; i < colorTab.length; i += 3) {
+ byte temp = colorTab[i];
+ colorTab[i] = colorTab[i + 2];
+ colorTab[i + 2] = temp;
+ usedEntry[i / 3] = false;
+ }
+ // map image pixels to new palette
+ int k = 0;
+ for (int i = 0; i < nPix; i++) {
+ int index =
+ nq.map(pixels[k++] & 0xff,
+ pixels[k++] & 0xff,
+ pixels[k++] & 0xff);
+ usedEntry[index] = true;
+ indexedPixels[i] = (byte)index;
+ }
+ pixels = null;
+ colorDepth = 8;
+ palSize = 7;
+ // get closest match to transparent color if specified
+ if (transparent != null) {
+ transIndex = transparentExactMatch ? findExact(transparent) : findClosest(transparent);
+ }
+ }
+
+ /**
+ * Returns index of palette color closest to c
+ *
+ * @param c Color
+ * @return index
+ */
+ protected int findClosest(Color c) {
+ if (colorTab == null)
+ return -1;
+ int r = c.getRed();
+ int g = c.getGreen();
+ int b = c.getBlue();
+ int minpos = 0;
+ int dmin = 256 * 256 * 256;
+ int len = colorTab.length;
+ for (int i = 0; i < len;) {
+ int dr = r - (colorTab[i++] & 0xff);
+ int dg = g - (colorTab[i++] & 0xff);
+ int db = b - (colorTab[i] & 0xff);
+ int d = dr * dr + dg * dg + db * db;
+ int index = i / 3;
+ if (usedEntry[index] && (d < dmin)) {
+ dmin = d;
+ minpos = index;
+ }
+ i++;
+ }
+ return minpos;
+ }
+
+ /**
+ * Returns true if the exact matching color is existing, and used in the color palette, otherwise, return false.
+ * This method has to be called before finishing the image,
+ * because after finished the palette is destroyed and it will always return false.
+ *
+ * @param c 颜色
+ * @return 颜色是否存在
+ */
+ boolean isColorUsed(Color c) {
+ return findExact(c) != -1;
+ }
+
+ /**
+ * Returns index of palette exactly matching to color c or -1 if there is no exact matching.
+ *
+ * @param c Color
+ * @return index
+ */
+ protected int findExact(Color c) {
+ if (colorTab == null) {
+ return -1;
+ }
+
+ int r = c.getRed();
+ int g = c.getGreen();
+ int b = c.getBlue();
+ int len = colorTab.length / 3;
+ for (int index = 0; index < len; ++index) {
+ int i = index * 3;
+ // If the entry is used in colorTab, then check if it is the same exact color we're looking for
+ if (usedEntry[index] && r == (colorTab[i] & 0xff) && g == (colorTab[i + 1] & 0xff) && b == (colorTab[i + 2] & 0xff)) {
+ return index;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Extracts image pixels into byte array "pixels"
+ */
+ protected void getImagePixels() {
+ int w = image.getWidth();
+ int h = image.getHeight();
+ int type = image.getType();
+ if ((w != width)
+ || (h != height)
+ || (type != BufferedImage.TYPE_3BYTE_BGR)) {
+ // create new image with right size/format
+ BufferedImage temp =
+ new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
+ Graphics2D g = temp.createGraphics();
+ g.setColor(background);
+ g.fillRect(0, 0, width, height);
+ g.drawImage(image, 0, 0, null);
+ image = temp;
+ }
+ pixels = ((DataBufferByte)image.getRaster().getDataBuffer()).getData();
+ }
+
+ /**
+ * Writes Graphic Control Extension
+ *
+ * @throws IOException IO异常
+ */
+ protected void writeGraphicCtrlExt() throws IOException {
+ out.write(0x21); // extension introducer
+ out.write(0xf9); // GCE label
+ out.write(4); // data block size
+ int transp, disp;
+ if (transparent == null) {
+ transp = 0;
+ disp = 0; // dispose = no action
+ } else {
+ transp = 1;
+ disp = 2; // force clear if using transparent color
+ }
+ if (dispose >= 0) {
+ disp = dispose & 7; // user override
+ }
+ disp <<= 2;
+
+ // packed fields
+ // noinspection PointlessBitwiseExpression
+ out.write(0 | // 1:3 reserved
+ disp | // 4:6 disposal
+ 0 | // 7 user input - 0 = none
+ transp); // 8 transparency flag
+
+ writeShort(delay); // delay x 1/100 sec
+ out.write(transIndex); // transparent color index
+ out.write(0); // block terminator
+ }
+
+ /**
+ * Writes Image Descriptor
+ *
+ * @throws IOException IO异常
+ */
+ protected void writeImageDesc() throws IOException {
+ out.write(0x2c); // image separator
+ writeShort(0); // image position x,y = 0,0
+ writeShort(0);
+ writeShort(width); // image size
+ writeShort(height);
+ // packed fields
+ if (firstFrame) {
+ // no LCT - GCT is used for first (or only) frame
+ out.write(0);
+ } else {
+ // specify normal LCT
+ // noinspection PointlessBitwiseExpression
+ out.write(0x80 | // 1 local color table 1=yes
+ 0 | // 2 interlace - 0=no
+ 0 | // 3 sorted - 0=no
+ 0 | // 4-5 reserved
+ palSize); // 6-8 size of color table
+ }
+ }
+
+ /**
+ * Writes Logical Screen Descriptor
+ *
+ * @throws IOException IO异常
+ */
+ protected void writeLSD() throws IOException {
+ // logical screen size
+ writeShort(width);
+ writeShort(height);
+ // packed fields
+ // noinspection PointlessBitwiseExpression
+ out.write((0x80 | // 1 : global color table flag = 1 (gct used)
+ 0x70 | // 2-4 : color resolution = 7
+ 0x00 | // 5 : gct sort flag = 0
+ palSize)); // 6-8 : gct size
+
+ out.write(0); // background color index
+ out.write(0); // pixel aspect ratio - assume 1:1
+ }
+
+ /**
+ * Writes Netscape application extension to define
+ * repeat count.
+ *
+ * @throws IOException IO异常
+ */
+ protected void writeNetscapeExt() throws IOException {
+ out.write(0x21); // extension introducer
+ out.write(0xff); // app extension label
+ out.write(11); // block size
+ writeString("NETSCAPE" + "2.0"); // app id + auth code
+ out.write(3); // sub-block size
+ out.write(1); // loop sub-block id
+ writeShort(repeat); // loop count (extra iterations, 0=repeat forever)
+ out.write(0); // block terminator
+ }
+
+ /**
+ * Writes color table
+ *
+ * @throws IOException IO异常
+ */
+ protected void writePalette() throws IOException {
+ out.write(colorTab, 0, colorTab.length);
+ int n = (3 * 256) - colorTab.length;
+ for (int i = 0; i < n; i++) {
+ out.write(0);
+ }
+ }
+
+ /**
+ * Encodes and writes pixel data
+ *
+ * @throws IOException IO异常
+ */
+ protected void writePixels() throws IOException {
+ LZWEncoder encoder = new LZWEncoder(width, height, indexedPixels, colorDepth);
+ encoder.encode(out);
+ }
+
+ /**
+ * Write 16-bit value to output stream, LSB first
+ *
+ * @param value 16-bit value
+ * @throws IOException IO异常
+ */
+ protected void writeShort(int value) throws IOException {
+ out.write(value & 0xff);
+ out.write((value >> 8) & 0xff);
+ }
+
+ /**
+ * Writes string to output stream
+ *
+ * @param s String
+ * @throws IOException IO异常
+ */
+ protected void writeString(String s) throws IOException {
+ for (int i = 0; i < s.length(); i++) {
+ out.write((byte)s.charAt(i));
+ }
+ }
+}
diff --git a/src/main/java/com/luna/common/img/gif/GifDecoder.java b/src/main/java/com/luna/common/img/gif/GifDecoder.java
new file mode 100755
index 000000000..61464cc6a
--- /dev/null
+++ b/src/main/java/com/luna/common/img/gif/GifDecoder.java
@@ -0,0 +1,784 @@
+package com.luna.common.img.gif;
+
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.awt.image.DataBufferInt;
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+
+import com.luna.common.io.IoUtil;
+
+/**
+ * GIF文件解析
+ * Class GifDecoder - Decodes a GIF file into one or more frames.
+ *
+ * Example:
+ *
+ *
+ * 来自:https://github.com/rtyley/animated-gif-lib-for-java
+ *
+ * @author Kevin Weiner, FM Software; LZW decoder adapted from John Cristy's ImageMagick.
+ */
+public class GifDecoder {
+
+ /**
+ * File read status: No errors.
+ */
+ public static final int STATUS_OK = 0;
+
+ /**
+ * File read status: Error decoding file (may be partially decoded)
+ */
+ public static final int STATUS_FORMAT_ERROR = 1;
+
+ /**
+ * File read status: Unable to open source.
+ */
+ public static final int STATUS_OPEN_ERROR = 2;
+ protected static final int MAX_STACK_SIZE = 4096;
+ protected BufferedInputStream in;
+ protected int status;
+ protected int width; // full image width
+ protected int height; // full image height
+ protected boolean gctFlag; // global color table used
+ protected int gctSize; // size of global color table
+ protected int loopCount = 1; // iterations; 0 = repeat forever
+ protected int[] gct; // global color table
+ protected int[] lct; // local color table
+ protected int[] act; // active color table
+ protected int bgIndex; // background color index
+ protected int bgColor; // background color
+ protected int lastBgColor; // previous bg color
+ protected int pixelAspect; // pixel aspect ratio
+ protected boolean lctFlag; // local color table flag
+ protected boolean interlace; // interlace flag
+ protected int lctSize; // local color table size
+ protected int ix, iy, iw, ih; // current image rectangle
+ protected Rectangle lastRect; // last image rect
+ protected BufferedImage image; // current frame
+ protected BufferedImage lastImage; // previous frame
+ protected byte[] block = new byte[256]; // current data block
+ protected int blockSize = 0; // block size
+ // last graphic control extension info
+ protected int dispose = 0;
+ // 0=no action; 1=leave in place; 2=restore to bg; 3=restore to prev
+ protected int lastDispose = 0;
+ protected boolean transparency = false; // use transparent color
+ protected int delay = 0; // delay in milliseconds
+ protected int transIndex; // transparent color index
+ // max decoder pixel stack size
+ // LZW decoder working arrays
+ protected short[] prefix;
+ protected byte[] suffix;
+ protected byte[] pixelStack;
+ protected byte[] pixels;
+
+ protected ArrayList
+ * 并发环境下,假设 test 目录不存在,如果线程A mkdirs "test/A" 目录,线程B mkdirs "test/B"目录,
+ * 其中一个线程可能会失败,进而导致以下代码抛出 FileNotFoundException 异常
+ *
+ * file.getParentFile().mkdirs(); // 父目录正在被另一个线程创建中,返回 false
+ * file.createNewFile(); // 抛出 IO 异常,因为该线程无法感知到父目录已被创建
+ *
+ *
+ * @param dir 待创建的目录
+ * @param tryCount 最大尝试次数
+ * @param sleepMillis 线程等待的毫秒数
+ * @return true表示创建成功,false表示创建失败
+ * @author z8g
+ * @since 5.7.21
+ */
+ public static boolean mkdirsSafely(File dir, int tryCount, long sleepMillis) {
+ if (dir == null) {
+ return false;
+ }
+ if (dir.isDirectory()) {
+ return true;
+ }
+ for (int i = 1; i <= tryCount; i++) { // 高并发场景下,可以看到 i 处于 1 ~ 3 之间
+ // 如果文件已存在,也会返回 false,所以该值不能作为是否能创建的依据,因此不对其进行处理
+ // noinspection ResultOfMethodCallIgnored
+ dir.mkdirs();
+ if (dir.exists()) {
+ return true;
+ }
+ try {
+ Thread.sleep(sleepMillis);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return dir.exists();
+ }
+
+ /**
+ * 创建所给文件或目录的父目录
+ *
+ * @param file 文件或目录
+ * @return 父目录
+ */
+ public static File mkParentDirs(File file) {
+ if (null == file) {
+ return null;
+ }
+ return mkdir(getParent(file, 1));
+ }
+
+ /**
+ * 获取指定层级的父路径
+ *
+ *
+ * getParent(file("d:/aaa/bbb/cc/ddd", 0)) -》 "d:/aaa/bbb/cc/ddd"
+ * getParent(file("d:/aaa/bbb/cc/ddd", 2)) -》 "d:/aaa/bbb"
+ * getParent(file("d:/aaa/bbb/cc/ddd", 4)) -》 "d:/"
+ * getParent(file("d:/aaa/bbb/cc/ddd", 5)) -》 null
+ *
+ *
+ * @param file 目录或文件
+ * @param level 层级
+ * @return 路径File,如果不存在返回null
+ * @since 4.1.2
+ */
+ public static File getParent(File file, int level) {
+ if (level < 1 || null == file) {
+ return file;
+ }
+
+ File parentFile;
+ try {
+ parentFile = file.getCanonicalFile().getParentFile();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ if (1 == level) {
+ return parentFile;
+ }
+ return getParent(parentFile, level - 1);
+ }
+
+ /**
+ * 创建文件夹,会递归自动创建其不存在的父文件夹,如果存在直接返回此文件夹
+ * 此方法不对File对象类型做判断,如果File不存在,无法判断其类型
+ *
+ * @param dir 目录
+ * @return 创建的目录
+ */
+ public static File mkdir(File dir) {
+ if (dir == null) {
+ return null;
+ }
+ if (false == dir.exists()) {
+ mkdirsSafely(dir, 5, 1);
+ }
+ return dir;
+ }
+
+ public static void copy(File src, File dest) {
+ try {
+ copy(src, dest, false, false);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static void copy(File src, File dest, Boolean isOverride, Boolean isCopyAttributes) throws IOException {
+ // check
+ Assert.notNull(src, "Source File is null !");
+ if (!src.exists()) {
+ throw new RuntimeException("File not exist: " + src);
+ }
+ Assert.notNull(dest, "Destination File or directiory is null !");
+ if (FileTools.equals(src, dest)) {
+ return;
+ }
+ final ArrayList
+ * 1. 颜色的英文名(大小写皆可)
+ * 2. 16进制表示,例如:#fcf6d6或者$fcf6d6
+ * 3. RGB形式,例如:13,148,252
+ *
+ *
+ * 首先尝试创建{@link Font#TRUETYPE_FONT}字体,此类字体无效则创建{@link Font#TYPE1_FONT}
+ *
+ * @param fontFile 字体文件
+ * @return {@link Font}
+ */
+ public static Font createFont(File fontFile) {
+ try {
+ return Font.createFont(Font.TRUETYPE_FONT, fontFile);
+ } catch (FontFormatException e) {
+ // True Type字体无效时使用Type1字体
+ try {
+ return Font.createFont(Font.TYPE1_FONT, fontFile);
+ } catch (Exception e1) {
+ throw new UtilException(e);
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * 根据文件创建字体
+ * 首先尝试创建{@link Font#TRUETYPE_FONT}字体,此类字体无效则创建{@link Font#TYPE1_FONT}
+ *
+ * @param fontStream 字体流
+ * @return {@link Font}
+ */
+ public static Font createFont(InputStream fontStream) {
+ try {
+ return Font.createFont(Font.TRUETYPE_FONT, fontStream);
+ } catch (FontFormatException e) {
+ // True Type字体无效时使用Type1字体
+ try {
+ return Font.createFont(Font.TYPE1_FONT, fontStream);
+ } catch (Exception e1) {
+ throw new UtilException(e1);
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * 获得字体对应字符串的长宽信息
+ *
+ * @param metrics {@link FontMetrics}
+ * @param str 字符串
+ * @return 长宽信息
+ */
+ public static Dimension getDimension(FontMetrics metrics, String str) {
+ final int width = metrics.stringWidth(str);
+ final int height = metrics.getAscent() - metrics.getLeading() - metrics.getDescent();
+
+ return new Dimension(width, height);
+ }
+
+}
diff --git a/src/main/java/com/luna/common/img/GraphicsUtil.java b/src/main/java/com/luna/common/img/GraphicsUtil.java
new file mode 100755
index 000000000..1e35178a0
--- /dev/null
+++ b/src/main/java/com/luna/common/img/GraphicsUtil.java
@@ -0,0 +1,208 @@
+package com.luna.common.img;
+
+import com.luna.common.utils.ObjectUtils;
+
+import java.awt.*;
+import java.awt.image.BufferedImage;
+
+/**
+ * {@link Graphics}相关工具类
+ *
+ * @author looly
+ * @since 4.5.2
+ */
+public class GraphicsUtil {
+
+ /**
+ * 创建{@link Graphics2D}
+ *
+ * @param image {@link BufferedImage}
+ * @param color {@link Color}背景颜色以及当前画笔颜色,{@code null}表示不设置背景色
+ * @return {@link Graphics2D}
+ * @since 4.5.2
+ */
+ public static Graphics2D createGraphics(BufferedImage image, Color color) {
+ final Graphics2D g = image.createGraphics();
+
+ if (null != color) {
+ // 填充背景
+ g.setColor(color);
+ g.fillRect(0, 0, image.getWidth(), image.getHeight());
+ }
+
+ return g;
+ }
+
+ /**
+ * 获取文字居中高度的Y坐标(距离上边距距离)
+ * 此方法依赖FontMetrics,如果获取失败,默认为背景高度的1/3
+ *
+ * @param g {@link Graphics2D}画笔
+ * @param backgroundHeight 背景高度
+ * @return 最小高度,-1表示无法获取
+ * @since 4.5.17
+ */
+ public static int getCenterY(Graphics g, int backgroundHeight) {
+ // 获取允许文字最小高度
+ FontMetrics metrics = null;
+ try {
+ metrics = g.getFontMetrics();
+ } catch (Exception e) {
+ // 此处报告bug某些情况下会抛出IndexOutOfBoundsException,在此做容错处理
+ }
+ int y;
+ if (null != metrics) {
+ y = (backgroundHeight - metrics.getHeight()) / 2 + metrics.getAscent();
+ } else {
+ y = backgroundHeight / 3;
+ }
+ return y;
+ }
+
+ /**
+ * 绘制字符串,使用随机颜色,默认抗锯齿
+ *
+ * @param g {@link Graphics}画笔
+ * @param str 字符串
+ * @param font 字体
+ * @param width 字符串总宽度
+ * @param height 字符串背景高度
+ * @return 画笔对象
+ * @since 4.5.10
+ */
+ public static Graphics drawStringColourful(Graphics g, String str, Font font, int width, int height) {
+ return drawString(g, str, font, null, width, height);
+ }
+
+ /**
+ * 绘制字符串,默认抗锯齿
+ *
+ * @param g {@link Graphics}画笔
+ * @param str 字符串
+ * @param font 字体
+ * @param color 字体颜色,{@code null} 表示使用随机颜色(每个字符单独随机)
+ * @param width 字符串背景的宽度
+ * @param height 字符串背景的高度
+ * @return 画笔对象
+ * @since 4.5.10
+ */
+ public static Graphics drawString(Graphics g, String str, Font font, Color color, int width, int height) {
+ // 抗锯齿
+ if (g instanceof Graphics2D) {
+ ((Graphics2D)g).setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+ }
+ // 创建字体
+ g.setFont(font);
+
+ // 文字高度(必须在设置字体后调用)
+ int midY = getCenterY(g, height);
+ if (null != color) {
+ g.setColor(color);
+ }
+
+ final int len = str.length();
+ int charWidth = width / len;
+ for (int i = 0; i < len; i++) {
+ if (null == color) {
+ // 产生随机的颜色值,让输出的每个字符的颜色值都将不同。
+ g.setColor(ImgUtil.randomColor());
+ }
+ g.drawString(String.valueOf(str.charAt(i)), i * charWidth, midY);
+ }
+ return g;
+ }
+
+ /**
+ * 绘制字符串,默认抗锯齿。
+ * 此方法定义一个矩形区域和坐标,文字基于这个区域中间偏移x,y绘制。
+ *
+ * @param g {@link Graphics}画笔
+ * @param str 字符串
+ * @param font 字体,字体大小决定了在背景中绘制的大小
+ * @param color 字体颜色,{@code null} 表示使用黑色
+ * @param rectangle 字符串绘制坐标和大小,此对象定义了绘制字符串的区域大小和偏移位置
+ * @return 画笔对象
+ * @since 4.5.10
+ */
+ public static Graphics drawString(Graphics g, String str, Font font, Color color, Rectangle rectangle) {
+ // 背景长宽
+ final int backgroundWidth = rectangle.width;
+ final int backgroundHeight = rectangle.height;
+
+ // 获取字符串本身的长宽
+ Dimension dimension;
+ try {
+ dimension = FontUtil.getDimension(g.getFontMetrics(font), str);
+ } catch (Exception e) {
+ // 此处报告bug某些情况下会抛出IndexOutOfBoundsException,在此做容错处理
+ dimension = new Dimension(backgroundWidth / 3, backgroundHeight / 3);
+ }
+
+ rectangle.setSize(dimension.width, dimension.height);
+ final Point point = ImgUtil.getPointBaseCentre(rectangle, backgroundWidth, backgroundHeight);
+
+ return drawString(g, str, font, color, point);
+ }
+
+ /**
+ * 绘制字符串,默认抗锯齿
+ *
+ * @param g {@link Graphics}画笔
+ * @param str 字符串
+ * @param font 字体,字体大小决定了在背景中绘制的大小
+ * @param color 字体颜色,{@code null} 表示使用黑色
+ * @param point 绘制字符串的位置坐标
+ * @return 画笔对象
+ * @since 5.3.6
+ */
+ public static Graphics drawString(Graphics g, String str, Font font, Color color, Point point) {
+ // 抗锯齿
+ if (g instanceof Graphics2D) {
+ ((Graphics2D)g).setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+ }
+
+ g.setFont(font);
+ g.setColor(ObjectUtils.defaultIfNull(color, Color.BLACK));
+ g.drawString(str, point.x, point.y);
+
+ return g;
+ }
+
+ /**
+ * 绘制图片
+ *
+ * @param g 画笔
+ * @param img 要绘制的图片
+ * @param point 绘制的位置,基于左上角
+ * @return 画笔对象
+ */
+ public static Graphics drawImg(Graphics g, Image img, Point point) {
+ return drawImg(g, img,
+ new Rectangle(point.x, point.y, img.getWidth(null), img.getHeight(null)));
+ }
+
+ /**
+ * 绘制图片
+ *
+ * @param g 画笔
+ * @param img 要绘制的图片
+ * @param rectangle 矩形对象,表示矩形区域的x,y,width,height,,基于左上角
+ * @return 画笔对象
+ */
+ public static Graphics drawImg(Graphics g, Image img, Rectangle rectangle) {
+ g.drawImage(img, rectangle.x, rectangle.y, rectangle.width, rectangle.height, null); // 绘制切割后的图
+ return g;
+ }
+
+ /**
+ * 设置画笔透明度
+ *
+ * @param g 画笔
+ * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字
+ * @return 画笔
+ */
+ public static Graphics2D setAlpha(Graphics2D g, float alpha) {
+ g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, alpha));
+ return g;
+ }
+}
diff --git a/src/main/java/com/luna/common/img/Img.java b/src/main/java/com/luna/common/img/Img.java
new file mode 100755
index 000000000..ec012d462
--- /dev/null
+++ b/src/main/java/com/luna/common/img/Img.java
@@ -0,0 +1,852 @@
+package com.luna.common.img;
+
+import com.luna.common.check.Assert;
+import com.luna.common.file.FileNameUtil;
+import com.luna.common.io.IoUtil;
+import com.luna.common.math.NumberUtil;
+import com.luna.common.text.StringTools;
+import com.luna.common.utils.ObjectUtils;
+
+import java.awt.*;
+import java.awt.color.ColorSpace;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Ellipse2D;
+import java.awt.geom.RoundRectangle2D;
+import java.awt.image.BufferedImage;
+import java.awt.image.CropImageFilter;
+import java.awt.image.ImageFilter;
+import java.io.*;
+import java.net.URL;
+import java.nio.file.Path;
+
+import javax.imageio.ImageIO;
+import javax.imageio.stream.ImageInputStream;
+import javax.imageio.stream.ImageOutputStream;
+
+/**
+ * 图像编辑器
+ *
+ * @author looly
+ * @since 4.1.5
+ */
+public class Img implements Flushable, Serializable {
+ private static final long serialVersionUID = 1L;
+
+ private final BufferedImage srcImage;
+ private Image targetImage;
+ /**
+ * 目标图片文件格式,用于写出
+ */
+ private String targetImageType;
+ /**
+ * 计算x,y坐标的时候是否从中心做为原始坐标开始计算
+ */
+ private boolean positionBaseCentre = true;
+ /**
+ * 图片输出质量,用于压缩
+ */
+ private float quality = -1;
+ /**
+ * 图片背景色
+ */
+ private Color backgroundColor;
+
+ /**
+ * 从Path读取图片并开始处理
+ *
+ * @param imagePath 图片文件路径
+ * @return Img
+ */
+ public static Img from(Path imagePath) {
+ return from(imagePath.toFile());
+ }
+
+ /**
+ * 从文件读取图片并开始处理
+ *
+ * @param imageFile 图片文件
+ * @return Img
+ */
+ public static Img from(File imageFile) {
+ return new Img(ImgUtil.read(imageFile));
+ }
+
+ /**
+ * 从流读取图片并开始处理
+ *
+ * @param in 图片流
+ * @return Img
+ */
+ public static Img from(InputStream in) {
+ return new Img(ImgUtil.read(in));
+ }
+
+ /**
+ * 从ImageInputStream取图片并开始处理
+ *
+ * @param imageStream 图片流
+ * @return Img
+ */
+ public static Img from(ImageInputStream imageStream) {
+ return new Img(ImgUtil.read(imageStream));
+ }
+
+ /**
+ * 从URL取图片并开始处理
+ *
+ * @param imageUrl 图片URL
+ * @return Img
+ */
+ public static Img from(URL imageUrl) {
+ return new Img(ImgUtil.read(imageUrl));
+ }
+
+ /**
+ * 从Image取图片并开始处理
+ *
+ * @param image 图片
+ * @return Img
+ */
+ public static Img from(Image image) {
+ return new Img(ImgUtil.castToBufferedImage(image, ImgUtil.IMAGE_TYPE_JPG));
+ }
+
+ /**
+ * 构造,目标图片类型取决于来源图片类型
+ *
+ * @param srcImage 来源图片
+ */
+ public Img(BufferedImage srcImage) {
+ this(srcImage, null);
+ }
+
+ /**
+ * 构造
+ *
+ * @param srcImage 来源图片
+ * @param targetImageType 目标图片类型,null则读取来源图片类型
+ * @since 5.0.7
+ */
+ public Img(BufferedImage srcImage, String targetImageType) {
+ this.srcImage = srcImage;
+ if (null == targetImageType) {
+ if (srcImage.getType() == BufferedImage.TYPE_INT_ARGB
+ || srcImage.getType() == BufferedImage.TYPE_INT_ARGB_PRE
+ || srcImage.getType() == BufferedImage.TYPE_4BYTE_ABGR
+ || srcImage.getType() == BufferedImage.TYPE_4BYTE_ABGR_PRE) {
+ targetImageType = ImgUtil.IMAGE_TYPE_PNG;
+ } else {
+ targetImageType = ImgUtil.IMAGE_TYPE_JPG;
+ }
+ }
+ this.targetImageType = targetImageType;
+ }
+
+ /**
+ * 设置目标图片文件格式,用于写出
+ *
+ * @param imgType 图片格式
+ * @return this
+ * @see ImgUtil#IMAGE_TYPE_JPG
+ * @see ImgUtil#IMAGE_TYPE_PNG
+ */
+ public Img setTargetImageType(String imgType) {
+ this.targetImageType = imgType;
+ return this;
+ }
+
+ /**
+ * 计算x,y坐标的时候是否从中心做为原始坐标开始计算
+ *
+ * @param positionBaseCentre 是否从中心做为原始坐标开始计算
+ * @return this
+ * @since 4.1.15
+ */
+ public Img setPositionBaseCentre(boolean positionBaseCentre) {
+ this.positionBaseCentre = positionBaseCentre;
+ return this;
+ }
+
+ /**
+ * 设置图片输出质量,数字为0~1(不包括0和1)表示质量压缩比,除此数字外设置表示不压缩
+ *
+ * @param quality 质量,数字为0~1(不包括0和1)表示质量压缩比,除此数字外设置表示不压缩
+ * @return this
+ * @since 4.3.2
+ */
+ public Img setQuality(double quality) {
+ return setQuality((float)quality);
+ }
+
+ /**
+ * 设置图片输出质量,数字为0~1(不包括0和1)表示质量压缩比,除此数字外设置表示不压缩
+ *
+ * @param quality 质量,数字为0~1(不包括0和1)表示质量压缩比,除此数字外设置表示不压缩
+ * @return this
+ * @since 4.3.2
+ */
+ public Img setQuality(float quality) {
+ if (quality > 0 && quality < 1) {
+ this.quality = quality;
+ } else {
+ this.quality = 1;
+ }
+ return this;
+ }
+
+ /**
+ * 设置图片的背景色
+ *
+ * @param backgroundColor{@link Color} 背景色
+ * @return this
+ */
+ public Img setBackgroundColor(Color backgroundColor) {
+ this.backgroundColor = backgroundColor;
+ return this;
+ }
+
+ /**
+ * 缩放图像(按比例缩放)
+ *
+ * @param scale 缩放比例。比例大于1时为放大,小于1大于0为缩小
+ * @return this
+ */
+ public Img scale(float scale) {
+ if (scale < 0) {
+ // 自动修正负数
+ scale = -scale;
+ }
+ final Image srcImg = getValidSrcImg();
+
+ // PNG图片特殊处理
+ if (ImgUtil.IMAGE_TYPE_PNG.equals(this.targetImageType)) {
+ // 修正float转double导致的精度丢失
+ final double scaleDouble = NumberUtil.parseDouble(String.valueOf(scale));
+ this.targetImage = ImgUtil.transform(AffineTransform.getScaleInstance(scaleDouble, scaleDouble),
+ ImgUtil.toBufferedImage(srcImg, this.targetImageType));
+ } else {
+ // 缩放后的图片宽
+ final int width = NumberUtil.mul((Number)srcImg.getWidth(null), scale).intValue();
+ // 缩放后的图片高
+ final int height = NumberUtil.mul((Number)srcImg.getHeight(null), scale).intValue();
+ scale(width, height);
+ }
+ return this;
+ }
+
+ /**
+ * 缩放图像(按长宽缩放)
+ * 注意:目标长宽与原图不成比例会变形
+ *
+ * @param width 目标宽度
+ * @param height 目标高度
+ * @return this
+ */
+ public Img scale(int width, int height) {
+ return scale(width, height, Image.SCALE_SMOOTH);
+ }
+
+ /**
+ * 缩放图像(按长宽缩放)
+ * 注意:目标长宽与原图不成比例会变形
+ *
+ * @param width 目标宽度
+ * @param height 目标高度
+ * @param scaleType 缩放类型,可选{@link Image#SCALE_SMOOTH}平滑模式或{@link Image#SCALE_DEFAULT}默认模式
+ * @return this
+ * @since 5.7.18
+ */
+ public Img scale(int width, int height, int scaleType) {
+ final Image srcImg = getValidSrcImg();
+
+ final int srcHeight = srcImg.getHeight(null);
+ final int srcWidth = srcImg.getWidth(null);
+ if (srcHeight == height && srcWidth == width) {
+ // 源与目标长宽一致返回原图
+ this.targetImage = srcImg;
+ return this;
+ }
+
+ if (ImgUtil.IMAGE_TYPE_PNG.equals(this.targetImageType)) {
+ // png特殊处理,借助AffineTransform可以实现透明度保留
+ final double sx = NumberUtil.div(width, srcWidth);// 宽度缩放比
+ final double sy = NumberUtil.div(height, srcHeight); // 高度缩放比
+ this.targetImage = ImgUtil.transform(AffineTransform.getScaleInstance(sx, sy),
+ ImgUtil.toBufferedImage(srcImg, this.targetImageType));
+ } else {
+ this.targetImage = srcImg.getScaledInstance(width, height, scaleType);
+ }
+
+ return this;
+ }
+
+ /**
+ * 等比缩放图像,此方法按照按照给定的长宽等比缩放图片,按照长宽缩放比最多的一边等比缩放,空白部分填充背景色
+ * 缩放后默认为jpeg格式
+ *
+ * @param width 缩放后的宽度
+ * @param height 缩放后的高度
+ * @param fixedColor 比例不对时补充的颜色,不补充为{@code null}
+ * @return this
+ */
+ public Img scale(int width, int height, Color fixedColor) {
+ Image srcImage = getValidSrcImg();
+ int srcHeight = srcImage.getHeight(null);
+ int srcWidth = srcImage.getWidth(null);
+ double heightRatio = NumberUtil.div(height, srcHeight);
+ double widthRatio = NumberUtil.div(width, srcWidth);
+
+ // 浮点数之间的等值判断,基本数据类型不能用==比较,包装数据类型不能用equals来判断。
+ if (NumberUtil.equals(heightRatio, widthRatio)) {
+ // 长宽都按照相同比例缩放时,返回缩放后的图片
+ scale(width, height);
+ } else if (widthRatio < heightRatio) {
+ // 宽缩放比例多就按照宽缩放
+ scale(width, (int)(srcHeight * widthRatio));
+ } else {
+ // 否则按照高缩放
+ scale((int)(srcWidth * heightRatio), height);
+ }
+
+ // 获取缩放后的新的宽和高
+ srcImage = getValidSrcImg();
+ srcHeight = srcImage.getHeight(null);
+ srcWidth = srcImage.getWidth(null);
+
+ final BufferedImage image = new BufferedImage(width, height, getTypeInt());
+ Graphics2D g = image.createGraphics();
+
+ // 设置背景
+ if (null != fixedColor) {
+ g.setBackground(fixedColor);
+ g.clearRect(0, 0, width, height);
+ }
+
+ // 在中间贴图
+ g.drawImage(srcImage, (width - srcWidth) / 2, (height - srcHeight) / 2, srcWidth, srcHeight, fixedColor, null);
+
+ g.dispose();
+ this.targetImage = image;
+ return this;
+ }
+
+ /**
+ * 图像切割(按指定起点坐标和宽高切割)
+ *
+ * @param rectangle 矩形对象,表示矩形区域的x,y,width,height
+ * @return this
+ */
+ public Img cut(Rectangle rectangle) {
+ final Image srcImage = getValidSrcImg();
+ fixRectangle(rectangle, srcImage.getWidth(null), srcImage.getHeight(null));
+
+ final ImageFilter cropFilter = new CropImageFilter(rectangle.x, rectangle.y, rectangle.width, rectangle.height);
+ this.targetImage = ImgUtil.filter(cropFilter, srcImage);
+ return this;
+ }
+
+ /**
+ * 图像切割为圆形(按指定起点坐标和半径切割),填充满整个图片(直径取长宽最小值)
+ *
+ * @param x 原图的x坐标起始位置
+ * @param y 原图的y坐标起始位置
+ * @return this
+ * @since 4.1.15
+ */
+ public Img cut(int x, int y) {
+ return cut(x, y, -1);
+ }
+
+ /**
+ * 图像切割为圆形(按指定起点坐标和半径切割)
+ *
+ * @param x 原图的x坐标起始位置
+ * @param y 原图的y坐标起始位置
+ * @param radius 半径,小于0表示填充满整个图片(直径取长宽最小值)
+ * @return this
+ * @since 4.1.15
+ */
+ public Img cut(int x, int y, int radius) {
+ final Image srcImage = getValidSrcImg();
+ final int width = srcImage.getWidth(null);
+ final int height = srcImage.getHeight(null);
+
+ // 计算直径
+ final int diameter = radius > 0 ? radius * 2 : Math.min(width, height);
+ final BufferedImage targetImage = new BufferedImage(diameter, diameter, BufferedImage.TYPE_INT_ARGB);
+ final Graphics2D g = targetImage.createGraphics();
+ g.setClip(new Ellipse2D.Double(0, 0, diameter, diameter));
+
+ if (this.positionBaseCentre) {
+ x = x - width / 2 + diameter / 2;
+ y = y - height / 2 + diameter / 2;
+ }
+ g.drawImage(srcImage, x, y, null);
+ g.dispose();
+ this.targetImage = targetImage;
+ return this;
+ }
+
+ /**
+ * 图片圆角处理
+ *
+ * @param arc 圆角弧度,0~1,为长宽占比
+ * @return this
+ * @since 4.5.3
+ */
+ public Img round(double arc) {
+ final Image srcImage = getValidSrcImg();
+ final int width = srcImage.getWidth(null);
+ final int height = srcImage.getHeight(null);
+
+ // 通过弧度占比计算弧度
+ arc = NumberUtil.mul(arc, Math.min(width, height));
+
+ final BufferedImage targetImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
+ final Graphics2D g2 = targetImage.createGraphics();
+ g2.setComposite(AlphaComposite.Src);
+ // 抗锯齿
+ g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+ g2.fill(new RoundRectangle2D.Double(0, 0, width, height, arc, arc));
+ g2.setComposite(AlphaComposite.SrcAtop);
+ g2.drawImage(srcImage, 0, 0, null);
+ g2.dispose();
+ this.targetImage = targetImage;
+ return this;
+ }
+
+ /**
+ * 彩色转为灰度
+ *
+ * @return this
+ */
+ public Img gray() {
+ this.targetImage = ImgUtil.colorConvert(ColorSpace.getInstance(ColorSpace.CS_GRAY), getValidSrcBufferedImg());
+ return this;
+ }
+
+ /**
+ * 彩色转为黑白二值化图片
+ *
+ * @return this
+ */
+ public Img binary() {
+ this.targetImage = ImgUtil.copyImage(getValidSrcImg(), BufferedImage.TYPE_BYTE_BINARY);
+ return this;
+ }
+
+ /**
+ * 给图片添加文字水印
+ * 此方法只在给定位置写出一个水印字符串
+ *
+ * @param pressText 水印文字
+ * @param color 水印的字体颜色
+ * @param font {@link Font} 字体相关信息
+ * @param x 修正值。 默认在中间,偏移量相对于中间偏移
+ * @param y 修正值。 默认在中间,偏移量相对于中间偏移
+ * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字
+ * @return 处理后的图像
+ */
+ public Img pressText(String pressText, Color color, Font font, int x, int y, float alpha) {
+ return pressText(pressText, color, font, new Point(x, y), alpha);
+ }
+
+ /**
+ * 给图片添加文字水印
+ * 此方法只在给定位置写出一个水印字符串
+ *
+ * @param pressText 水印文字
+ * @param color 水印的字体颜色
+ * @param font {@link Font} 字体相关信息
+ * @param point 绘制字符串的位置坐标
+ * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字
+ * @return 处理后的图像
+ */
+ public Img pressText(String pressText, Color color, Font font, Point point, float alpha) {
+ final BufferedImage targetImage = ImgUtil.toBufferedImage(getValidSrcImg(), this.targetImageType);
+
+ if (null == font) {
+ // 默认字体
+ font = FontUtil.createSansSerifFont((int)(targetImage.getHeight() * 0.75));
+ }
+
+ final Graphics2D g = targetImage.createGraphics();
+ // 透明度
+ g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, alpha));
+
+ // 绘制
+ if (positionBaseCentre) {
+ // 基于中心绘制
+ GraphicsUtil.drawString(g, pressText, font, color,
+ new Rectangle(point.x, point.y, targetImage.getWidth(), targetImage.getHeight()));
+ } else {
+ // 基于左上角绘制
+ GraphicsUtil.drawString(g, pressText, font, color, point);
+ }
+
+ // 收笔
+ g.dispose();
+ this.targetImage = targetImage;
+
+ return this;
+ }
+
+ /**
+ * 给图片添加全屏文字水印
+ *
+ * @param pressText 水印文字,文件间的间隔使用尾部添加空格方式实现
+ * @param color 水印的字体颜色
+ * @param font {@link Font} 字体相关信息
+ * @param lineHeight 行高
+ * @param degree 旋转角度,(单位:弧度),以圆点(0,0)为圆心,正代表顺时针,负代表逆时针
+ * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字
+ * @return 处理后的图像
+ * @since 5.8.0
+ */
+ public Img pressTextFull(String pressText, Color color, Font font, int lineHeight, int degree, float alpha) {
+ final BufferedImage targetImage = ImgUtil.toBufferedImage(getValidSrcImg(), this.targetImageType);
+
+ if (null == font) {
+ // 默认字体
+ font = FontUtil.createSansSerifFont((int)(targetImage.getHeight() * 0.75));
+ }
+ final int targetHeight = targetImage.getHeight();
+ final int targetWidth = targetImage.getWidth();
+
+ // 创建画笔,并设置透明度和角度
+ final Graphics2D g = targetImage.createGraphics();
+ g.setColor(color);
+ // 基于图片中心旋转
+ g.rotate(Math.toRadians(degree), targetWidth >> 1, targetHeight >> 1);
+ g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, alpha));
+
+ // 获取字符串本身的长宽
+ Dimension dimension;
+ try {
+ dimension = FontUtil.getDimension(g.getFontMetrics(font), pressText);
+ } catch (Exception e) {
+ // 此处报告bug某些情况下会抛出IndexOutOfBoundsException,在此做容错处理
+ dimension = new Dimension(targetWidth / 3, targetHeight / 3);
+ }
+ final int intervalHeight = dimension.height * lineHeight;
+ // 在画笔按照画布中心旋转后,达到45度时,上下左右会出现空白区,此处各延申长款的1.5倍实现全覆盖
+ int y = -targetHeight >> 1;
+ while (y < targetHeight * 1.5) {
+ int x = -targetWidth >> 1;
+ while (x < targetWidth * 1.5) {
+ GraphicsUtil.drawString(g, pressText, font, color, new Point(x, y));
+ x += dimension.width;
+ }
+ y += intervalHeight;
+ }
+ g.dispose();
+
+ this.targetImage = targetImage;
+ return this;
+ }
+
+ /**
+ * 给图片添加图片水印
+ *
+ * @param pressImg 水印图片,可以使用{@link ImageIO#read(File)}方法读取文件
+ * @param x 修正值。 默认在中间,偏移量相对于中间偏移
+ * @param y 修正值。 默认在中间,偏移量相对于中间偏移
+ * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字
+ * @return this
+ */
+ public Img pressImage(Image pressImg, int x, int y, float alpha) {
+ final int pressImgWidth = pressImg.getWidth(null);
+ final int pressImgHeight = pressImg.getHeight(null);
+ return pressImage(pressImg, new Rectangle(x, y, pressImgWidth, pressImgHeight), alpha);
+ }
+
+ /**
+ * 给图片添加图片水印
+ *
+ * @param pressImg 水印图片,可以使用{@link ImageIO#read(File)}方法读取文件
+ * @param rectangle 矩形对象,表示矩形区域的x,y,width,height,x,y从背景图片中心计算
+ * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字
+ * @return this
+ * @since 4.1.14
+ */
+ public Img pressImage(Image pressImg, Rectangle rectangle, float alpha) {
+ final Image targetImg = getValidSrcImg();
+
+ this.targetImage = draw(ImgUtil.toBufferedImage(targetImg, this.targetImageType), pressImg, rectangle, alpha);
+ return this;
+ }
+
+ /**
+ * 旋转图片为指定角度
+ * 来自:http://blog.51cto.com/cping1982/130066
+ *
+ * @param degree 旋转角度
+ * @return 旋转后的图片
+ * @since 3.2.2
+ */
+ public Img rotate(int degree) {
+ final Image image = getValidSrcImg();
+ int width = image.getWidth(null);
+ int height = image.getHeight(null);
+ final Rectangle rectangle = calcRotatedSize(width, height, degree);
+ final BufferedImage targetImg = new BufferedImage(rectangle.width, rectangle.height, getTypeInt());
+ Graphics2D graphics2d = targetImg.createGraphics();
+ // 抗锯齿
+ graphics2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
+ // 从中心旋转
+ graphics2d.translate((rectangle.width - width) / 2D, (rectangle.height - height) / 2D);
+ graphics2d.rotate(Math.toRadians(degree), width / 2D, height / 2D);
+ graphics2d.drawImage(image, 0, 0, null);
+ graphics2d.dispose();
+ this.targetImage = targetImg;
+ return this;
+ }
+
+ /**
+ * 水平翻转图像
+ *
+ * @return this
+ */
+ public Img flip() {
+ final Image image = getValidSrcImg();
+ int width = image.getWidth(null);
+ int height = image.getHeight(null);
+ final BufferedImage targetImg = new BufferedImage(width, height, getTypeInt());
+ Graphics2D graphics2d = targetImg.createGraphics();
+ graphics2d.drawImage(image, 0, 0, width, height, width, 0, 0, height, null);
+ graphics2d.dispose();
+
+ this.targetImage = targetImg;
+ return this;
+ }
+
+ /**
+ * 描边,此方法为向内描边,会覆盖图片相应的位置
+ *
+ * @param color 描边颜色,默认黑色
+ * @param width 边框粗细
+ * @return this
+ * @since 5.4.1
+ */
+ public Img stroke(Color color, float width) {
+ return stroke(color, new BasicStroke(width));
+ }
+
+ /**
+ * 描边,此方法为向内描边,会覆盖图片相应的位置
+ *
+ * @param color 描边颜色,默认黑色
+ * @param stroke 描边属性,包括粗细、线条类型等,见{@link BasicStroke}
+ * @return this
+ * @since 5.4.1
+ */
+ public Img stroke(Color color, Stroke stroke) {
+ final BufferedImage image = ImgUtil.toBufferedImage(getValidSrcImg(), this.targetImageType);
+ int width = image.getWidth(null);
+ int height = image.getHeight(null);
+ Graphics2D g = image.createGraphics();
+
+ g.setColor(ObjectUtils.defaultIfNull(color, Color.BLACK));
+ if (null != stroke) {
+ g.setStroke(stroke);
+ }
+
+ g.drawRect(0, 0, width - 1, height - 1);
+
+ g.dispose();
+ this.targetImage = image;
+
+ return this;
+ }
+
+ // -----------------------------------------------------------------------------------------------------------------
+ // Write
+
+ /**
+ * 获取处理过的图片
+ *
+ * @return 处理过的图片
+ */
+ public Image getImg() {
+ return getValidSrcImg();
+ }
+
+ /**
+ * 写出图像为结果设置格式
+ * 结果类型设定见{@link #setTargetImageType(String)}
+ *
+ * @param out 写出到的目标流
+ * @return 是否成功写出,如果返回false表示未找到合适的Writer
+ * @throws RuntimeException IO异常
+ */
+ public boolean write(OutputStream out) throws RuntimeException {
+ return write(ImgUtil.getImageOutputStream(out));
+ }
+
+ /**
+ * 写出图像为结果设置格式
+ * 结果类型设定见{@link #setTargetImageType(String)}
+ *
+ * @param targetImageStream 写出到的目标流
+ * @return 是否成功写出,如果返回false表示未找到合适的Writer
+ * @throws RuntimeException IO异常
+ */
+ public boolean write(ImageOutputStream targetImageStream) throws RuntimeException {
+ Assert.notNull(this.targetImageType, "Target image type is blank !");
+ Assert.notNull(targetImageStream, "Target output stream is null !");
+
+ final Image targetImage = (null == this.targetImage) ? this.srcImage : this.targetImage;
+ Assert.notNull(targetImage, "Target image is null !");
+
+ return ImgUtil.write(targetImage, this.targetImageType, targetImageStream, this.quality, this.backgroundColor);
+ }
+
+ /**
+ * 写出图像为目标文件扩展名对应的格式
+ *
+ * @param targetFile 目标文件
+ * @return 是否成功写出,如果返回false表示未找到合适的Writer
+ * @throws RuntimeException IO异常
+ */
+ public boolean write(File targetFile) throws RuntimeException {
+ final String formatName = FileNameUtil.extName(targetFile);
+ if (StringTools.isNotBlank(formatName)) {
+ this.targetImageType = formatName;
+ }
+
+ if (targetFile.exists()) {
+ // noinspection ResultOfMethodCallIgnored
+ targetFile.delete();
+ }
+
+ ImageOutputStream out = null;
+ try {
+ out = ImgUtil.getImageOutputStream(targetFile);
+ return write(out);
+ } finally {
+ IoUtil.close(out);
+ }
+ }
+
+ @Override
+ public void flush() {
+ ImgUtil.flush(this.srcImage);
+ ImgUtil.flush(this.targetImage);
+ }
+
+ // ----------------------------------------------------------------------------------------------------------------
+ // Private method start
+
+ /**
+ * 将图片绘制在背景上
+ *
+ * @param backgroundImg 背景图片
+ * @param img 要绘制的图片
+ * @param rectangle 矩形对象,表示矩形区域的x,y,width,height,x,y从背景图片中心计算(如果positionBaseCentre为true)
+ * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字
+ * @return 绘制后的背景
+ */
+ private BufferedImage draw(BufferedImage backgroundImg, Image img, Rectangle rectangle, float alpha) {
+ final Graphics2D g = backgroundImg.createGraphics();
+ GraphicsUtil.setAlpha(g, alpha);
+
+ fixRectangle(rectangle, backgroundImg.getWidth(), backgroundImg.getHeight());
+ GraphicsUtil.drawImg(g, img, rectangle);
+
+ g.dispose();
+ return backgroundImg;
+ }
+
+ /**
+ * 获取int类型的图片类型
+ *
+ * @return 图片类型
+ * @see BufferedImage#TYPE_INT_ARGB
+ * @see BufferedImage#TYPE_INT_RGB
+ */
+ private int getTypeInt() {
+ // noinspection SwitchStatementWithTooFewBranches
+ switch (this.targetImageType) {
+ case ImgUtil.IMAGE_TYPE_PNG:
+ return BufferedImage.TYPE_INT_ARGB;
+ default:
+ return BufferedImage.TYPE_INT_RGB;
+ }
+ }
+
+ /**
+ * 获取有效的源图片,首先检查上一次处理的结果图片,如无则使用用户传入的源图片
+ *
+ * @return 有效的源图片
+ */
+ private Image getValidSrcImg() {
+ return ObjectUtils.defaultIfNull(this.targetImage, this.srcImage);
+ }
+
+ /**
+ * 获取有效的源{@link BufferedImage}图片,首先检查上一次处理的结果图片,如无则使用用户传入的源图片
+ *
+ * @return 有效的源图片
+ * @since 5.7.8
+ */
+ private BufferedImage getValidSrcBufferedImg() {
+ return ImgUtil.toBufferedImage(getValidSrcImg(), this.targetImageType);
+ }
+
+ /**
+ * 修正矩形框位置,如果{@link Img#setPositionBaseCentre(boolean)} 设为{@code true},
+ * 则坐标修正为基于图形中心,否则基于左上角
+ *
+ * @param rectangle 矩形
+ * @param baseWidth 参考宽
+ * @param baseHeight 参考高
+ * @return 修正后的{@link Rectangle}
+ * @since 4.1.15
+ */
+ private Rectangle fixRectangle(Rectangle rectangle, int baseWidth, int baseHeight) {
+ if (this.positionBaseCentre) {
+ final Point pointBaseCentre = ImgUtil.getPointBaseCentre(rectangle, baseWidth, baseHeight);
+ // 修正图片位置从背景的中心计算
+ rectangle.setLocation(pointBaseCentre.x, pointBaseCentre.y);
+ }
+ return rectangle;
+ }
+
+ /**
+ * 计算旋转后的图片尺寸
+ *
+ * @param width 宽度
+ * @param height 高度
+ * @param degree 旋转角度
+ * @return 计算后目标尺寸
+ * @since 4.1.20
+ */
+ private static Rectangle calcRotatedSize(int width, int height, int degree) {
+ if (degree < 0) {
+ // 负数角度转换为正数角度
+ degree += 360;
+ }
+ if (degree >= 90) {
+ if (degree / 90 % 2 == 1) {
+ int temp = height;
+ // noinspection SuspiciousNameCombination
+ height = width;
+ width = temp;
+ }
+ degree = degree % 90;
+ }
+ double r = Math.sqrt(height * height + width * width) / 2;
+ double len = 2 * Math.sin(Math.toRadians(degree) / 2) * r;
+ double angel_alpha = (Math.PI - Math.toRadians(degree)) / 2;
+ double angel_dalta_width = Math.atan((double)height / width);
+ double angel_dalta_height = Math.atan((double)width / height);
+ int len_dalta_width = (int)(len * Math.cos(Math.PI - angel_alpha - angel_dalta_width));
+ int len_dalta_height = (int)(len * Math.cos(Math.PI - angel_alpha - angel_dalta_height));
+ int des_width = width + len_dalta_width * 2;
+ int des_height = height + len_dalta_height * 2;
+
+ return new Rectangle(des_width, des_height);
+ }
+ // ----------------------------------------------------------------------------------------------------------------
+ // Private method end
+}
diff --git a/src/main/java/com/luna/common/img/ImgUtil.java b/src/main/java/com/luna/common/img/ImgUtil.java
new file mode 100755
index 000000000..1cd834fb4
--- /dev/null
+++ b/src/main/java/com/luna/common/img/ImgUtil.java
@@ -0,0 +1,2469 @@
+package com.luna.common.img;
+
+import com.luna.common.check.Assert;
+import com.luna.common.encrypt.Base64Util;
+import com.luna.common.file.FileNameUtil;
+import com.luna.common.file.FileNameUtils;
+import com.luna.common.file.FileTools;
+import com.luna.common.io.IoUtil;
+import com.luna.common.math.NumberUtil;
+import com.luna.common.text.StringTools;
+import com.luna.common.utils.ObjectUtils;
+import org.apache.commons.io.FileUtils;
+
+import java.awt.*;
+import java.awt.color.ColorSpace;
+import java.awt.font.FontRenderContext;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Rectangle2D;
+import java.awt.image.*;
+import java.io.*;
+import java.net.URL;
+import java.nio.charset.Charset;
+import java.util.Iterator;
+import java.util.Random;
+
+import javax.imageio.*;
+import javax.imageio.stream.ImageInputStream;
+import javax.imageio.stream.ImageOutputStream;
+import javax.swing.*;
+
+/**
+ * 图片处理工具类:
+ * 功能:缩放图像、切割图像、旋转、图像类型转换、彩色转黑白、文字水印、图片水印等
+ * 参考:http://blog.csdn.net/zhangzhikaixinya/article/details/8459400
+ *
+ * @author Looly
+ */
+public class ImgUtil {
+
+ // region ----- [const] image type
+ /**
+ * 图形交换格式:GIF
+ */
+ public static final String IMAGE_TYPE_GIF = "gif";
+ /**
+ * 联合照片专家组:JPG
+ */
+ public static final String IMAGE_TYPE_JPG = "jpg";
+ /**
+ * 联合照片专家组:JPEG
+ */
+ public static final String IMAGE_TYPE_JPEG = "jpeg";
+ /**
+ * 英文Bitmap(位图)的简写,它是Windows操作系统中的标准图像文件格式:BMP
+ */
+ public static final String IMAGE_TYPE_BMP = "bmp";
+ /**
+ * 可移植网络图形:PNG
+ */
+ public static final String IMAGE_TYPE_PNG = "png";
+ /**
+ * Photoshop的专用格式:PSD
+ */
+ public static final String IMAGE_TYPE_PSD = "psd";
+ // endregion
+
+ // ----------------------------------------------------------------------------------------------------------------------
+ // scale
+
+ /**
+ * 缩放图像(按比例缩放),目标文件的扩展名决定目标文件类型
+ *
+ * @param srcImageFile 源图像文件
+ * @param destImageFile 缩放后的图像文件,扩展名决定目标类型
+ * @param scale 缩放比例。比例大于1时为放大,小于1大于0为缩小
+ */
+ public static void scale(File srcImageFile, File destImageFile, float scale) {
+ BufferedImage image = null;
+ try {
+ image = read(srcImageFile);
+ scale(image, destImageFile, scale);
+ } finally {
+ flush(image);
+ }
+ }
+
+ /**
+ * 缩放图像(按比例缩放)
+ * 缩放后默认为jpeg格式,此方法并不关闭流
+ *
+ * @param srcStream 源图像来源流
+ * @param destStream 缩放后的图像写出到的流
+ * @param scale 缩放比例。比例大于1时为放大,小于1大于0为缩小
+ * @since 3.0.9
+ */
+ public static void scale(InputStream srcStream, OutputStream destStream, float scale) {
+ BufferedImage image = null;
+ try {
+ image = read(srcStream);
+ scale(image, destStream, scale);
+ } finally {
+ flush(image);
+ }
+ }
+
+ /**
+ * 缩放图像(按比例缩放)
+ * 缩放后默认为jpeg格式,此方法并不关闭流
+ *
+ * @param srcStream 源图像来源流
+ * @param destStream 缩放后的图像写出到的流
+ * @param scale 缩放比例。比例大于1时为放大,小于1大于0为缩小
+ * @since 3.1.0
+ */
+ public static void scale(ImageInputStream srcStream, ImageOutputStream destStream, float scale) {
+ BufferedImage image = null;
+ try {
+ image = read(srcStream);
+ scale(image, destStream, scale);
+ } finally {
+ flush(image);
+ }
+ }
+
+ /**
+ * 缩放图像(按比例缩放)
+ * 缩放后默认为jpeg格式,此方法并不关闭流
+ *
+ * @param srcImg 源图像来源流,使用结束后需手动调用{@link #flush(Image)}释放资源
+ * @param destFile 缩放后的图像写出到的流
+ * @param scale 缩放比例。比例大于1时为放大,小于1大于0为缩小
+ * @throws RuntimeException IO异常
+ * @since 3.2.2
+ */
+ public static void scale(Image srcImg, File destFile, float scale) throws RuntimeException {
+ Img.from(srcImg).setTargetImageType(FileNameUtil.extName(destFile)).scale(scale).write(destFile);
+ }
+
+ /**
+ * 缩放图像(按比例缩放)
+ * 缩放后默认为jpeg格式,此方法并不关闭流
+ *
+ * @param srcImg 源图像来源流,使用结束后需手动调用{@link #flush(Image)}释放资源
+ * @param out 缩放后的图像写出到的流
+ * @param scale 缩放比例。比例大于1时为放大,小于1大于0为缩小
+ * @throws RuntimeException IO异常
+ * @since 3.2.2
+ */
+ public static void scale(Image srcImg, OutputStream out, float scale) throws RuntimeException {
+ scale(srcImg, getImageOutputStream(out), scale);
+ }
+
+ /**
+ * 缩放图像(按比例缩放)
+ * 缩放后默认为jpeg格式,此方法并不关闭流
+ *
+ * @param srcImg 源图像来源流,使用结束后需手动调用{@link #flush(Image)}释放资源
+ * @param destImageStream 缩放后的图像写出到的流
+ * @param scale 缩放比例。比例大于1时为放大,小于1大于0为缩小
+ * @throws RuntimeException IO异常
+ * @since 3.1.0
+ */
+ public static void scale(Image srcImg, ImageOutputStream destImageStream, float scale) throws RuntimeException {
+ writeJpg(scale(srcImg, scale), destImageStream);
+ }
+
+ /**
+ * 缩放图像(按比例缩放)
+ *
+ * @param srcImg 源图像来源流,使用结束后需手动调用{@link #flush(Image)}释放资源
+ * @param scale 缩放比例。比例大于1时为放大,小于1大于0为缩小
+ * @return {@link Image}
+ * @since 3.1.0
+ */
+ public static Image scale(Image srcImg, float scale) {
+ return Img.from(srcImg).scale(scale).getImg();
+ }
+
+ /**
+ * 缩放图像(按长宽缩放)
+ * 注意:目标长宽与原图不成比例会变形
+ *
+ * @param srcImg 源图像来源流,使用结束后需手动调用{@link #flush(Image)}释放资源
+ * @param width 目标宽度
+ * @param height 目标高度
+ * @return {@link Image}
+ * @since 3.1.0
+ */
+ public static Image scale(Image srcImg, int width, int height) {
+ return Img.from(srcImg).scale(width, height).getImg();
+ }
+
+ /**
+ * 缩放图像(按高度和宽度缩放)
+ * 缩放后默认格式与源图片相同,无法识别原图片默认JPG
+ *
+ * @param srcImageFile 源图像文件地址
+ * @param destImageFile 缩放后的图像地址
+ * @param width 缩放后的宽度
+ * @param height 缩放后的高度
+ * @param fixedColor 补充的颜色,不补充为{@code null}
+ * @throws RuntimeException IO异常
+ */
+ public static void scale(File srcImageFile, File destImageFile, int width, int height, Color fixedColor) throws RuntimeException {
+ Img img = null;
+ try {
+ img = Img.from(srcImageFile);
+ img.setTargetImageType(FileNameUtil.extName(destImageFile))
+ .scale(width, height, fixedColor)//
+ .write(destImageFile);
+ } finally {
+ IoUtil.flush(img);
+ }
+ }
+
+ /**
+ * 缩放图像(按高度和宽度缩放)
+ * 缩放后默认为jpeg格式,此方法并不关闭流
+ *
+ * @param srcStream 源图像流
+ * @param destStream 缩放后的图像目标流
+ * @param width 缩放后的宽度
+ * @param height 缩放后的高度
+ * @param fixedColor 比例不对时补充的颜色,不补充为{@code null}
+ * @throws RuntimeException IO异常
+ */
+ public static void scale(InputStream srcStream, OutputStream destStream, int width, int height, Color fixedColor) throws RuntimeException {
+ BufferedImage image = null;
+ try {
+ image = read(srcStream);
+ scale(image, getImageOutputStream(destStream), width, height, fixedColor);
+ } finally {
+ flush(image);
+ }
+ }
+
+ /**
+ * 缩放图像(按高度和宽度缩放)
+ * 缩放后默认为jpeg格式,此方法并不关闭流
+ *
+ * @param srcStream 源图像流
+ * @param destStream 缩放后的图像目标流
+ * @param width 缩放后的宽度
+ * @param height 缩放后的高度
+ * @param fixedColor 比例不对时补充的颜色,不补充为{@code null}
+ * @throws RuntimeException IO异常
+ */
+ public static void scale(ImageInputStream srcStream, ImageOutputStream destStream, int width, int height, Color fixedColor)
+ throws RuntimeException {
+ BufferedImage image = null;
+ try {
+ image = read(srcStream);
+ scale(image, destStream, width, height, fixedColor);
+ } finally {
+ flush(image);
+ }
+ }
+
+ /**
+ * 缩放图像(按高度和宽度缩放)
+ * 缩放后默认为jpeg格式,此方法并不关闭流
+ *
+ * @param srcImage 源图像
+ * @param destImageStream 缩放后的图像目标流
+ * @param width 缩放后的宽度
+ * @param height 缩放后的高度
+ * @param fixedColor 比例不对时补充的颜色,不补充为{@code null}
+ * @throws RuntimeException IO异常
+ */
+ public static void scale(Image srcImage, ImageOutputStream destImageStream, int width, int height, Color fixedColor) throws RuntimeException {
+ writeJpg(scale(srcImage, width, height, fixedColor), destImageStream);
+ }
+
+ /**
+ * 缩放图像(按高度和宽度缩放)
+ * 缩放后默认为jpeg格式
+ *
+ * @param srcImage 源图像
+ * @param width 缩放后的宽度
+ * @param height 缩放后的高度
+ * @param fixedColor 比例不对时补充的颜色,不补充为{@code null}
+ * @return {@link Image}
+ */
+ public static Image scale(Image srcImage, int width, int height, Color fixedColor) {
+ return Img.from(srcImage).scale(width, height, fixedColor).getImg();
+ }
+
+ // ----------------------------------------------------------------------------------------------------------------------
+ // cut
+
+ /**
+ * 图像切割(按指定起点坐标和宽高切割)
+ *
+ * @param srcImgFile 源图像文件
+ * @param destImgFile 切片后的图像文件
+ * @param rectangle 矩形对象,表示矩形区域的x,y,width,height
+ * @since 3.1.0
+ */
+ public static void cut(File srcImgFile, File destImgFile, Rectangle rectangle) {
+ BufferedImage image = null;
+ try {
+ image = read(srcImgFile);
+ cut(image, destImgFile, rectangle);
+ } finally {
+ flush(image);
+ }
+ }
+
+ /**
+ * 图像切割(按指定起点坐标和宽高切割),此方法并不关闭流
+ *
+ * @param srcStream 源图像流
+ * @param destStream 切片后的图像输出流
+ * @param rectangle 矩形对象,表示矩形区域的x,y,width,height
+ * @since 3.1.0
+ */
+ public static void cut(InputStream srcStream, OutputStream destStream, Rectangle rectangle) {
+ BufferedImage image = null;
+ try {
+ image = read(srcStream);
+ cut(image, destStream, rectangle);
+ } finally {
+ flush(image);
+ }
+ }
+
+ /**
+ * 图像切割(按指定起点坐标和宽高切割),此方法并不关闭流
+ *
+ * @param srcStream 源图像流
+ * @param destStream 切片后的图像输出流
+ * @param rectangle 矩形对象,表示矩形区域的x,y,width,height
+ * @since 3.1.0
+ */
+ public static void cut(ImageInputStream srcStream, ImageOutputStream destStream, Rectangle rectangle) {
+ BufferedImage image = null;
+ try {
+ image = read(srcStream);
+ cut(image, destStream, rectangle);
+ } finally {
+ flush(image);
+ }
+ }
+
+ /**
+ * 图像切割(按指定起点坐标和宽高切割),此方法并不关闭流
+ *
+ * @param srcImage 源图像,使用结束后需手动调用{@link #flush(Image)}释放资源
+ * @param destFile 输出的文件
+ * @param rectangle 矩形对象,表示矩形区域的x,y,width,height
+ * @throws RuntimeException IO异常
+ * @since 3.2.2
+ */
+ public static void cut(Image srcImage, File destFile, Rectangle rectangle) throws RuntimeException {
+ write(cut(srcImage, rectangle), destFile);
+ }
+
+ /**
+ * 图像切割(按指定起点坐标和宽高切割),此方法并不关闭流
+ *
+ * @param srcImage 源图像,使用结束后需手动调用{@link #flush(Image)}释放资源
+ * @param out 切片后的图像输出流
+ * @param rectangle 矩形对象,表示矩形区域的x,y,width,height
+ * @throws RuntimeException IO异常
+ * @since 3.1.0
+ */
+ public static void cut(Image srcImage, OutputStream out, Rectangle rectangle) throws RuntimeException {
+ cut(srcImage, getImageOutputStream(out), rectangle);
+ }
+
+ /**
+ * 图像切割(按指定起点坐标和宽高切割),此方法并不关闭流
+ *
+ * @param srcImage 源图像,使用结束后需手动调用{@link #flush(Image)}释放资源
+ * @param destImageStream 切片后的图像输出流
+ * @param rectangle 矩形对象,表示矩形区域的x,y,width,height
+ * @throws RuntimeException IO异常
+ * @since 3.1.0
+ */
+ public static void cut(Image srcImage, ImageOutputStream destImageStream, Rectangle rectangle) throws RuntimeException {
+ writeJpg(cut(srcImage, rectangle), destImageStream);
+ }
+
+ /**
+ * 图像切割(按指定起点坐标和宽高切割)
+ *
+ * @param srcImage 源图像,使用结束后需手动调用{@link #flush(Image)}释放资源
+ * @param rectangle 矩形对象,表示矩形区域的x,y,width,height
+ * @return {@link BufferedImage}
+ * @since 3.1.0
+ */
+ public static Image cut(Image srcImage, Rectangle rectangle) {
+ return Img.from(srcImage).setPositionBaseCentre(false).cut(rectangle).getImg();
+ }
+
+ /**
+ * 图像切割(按指定起点坐标和宽高切割),填充满整个图片(直径取长宽最小值)
+ *
+ * @param srcImage 源图像,使用结束后需手动调用{@link #flush(Image)}释放资源
+ * @param x 原图的x坐标起始位置
+ * @param y 原图的y坐标起始位置
+ * @return {@link Image}
+ * @since 4.1.15
+ */
+ public static Image cut(Image srcImage, int x, int y) {
+ return cut(srcImage, x, y, -1);
+ }
+
+ /**
+ * 图像切割(按指定起点坐标和宽高切割)
+ *
+ * @param srcImage 源图像,使用结束后需手动调用{@link #flush(Image)}释放资源
+ * @param x 原图的x坐标起始位置
+ * @param y 原图的y坐标起始位置
+ * @param radius 半径,小于0表示填充满整个图片(直径取长宽最小值)
+ * @return {@link Image}
+ * @since 4.1.15
+ */
+ public static Image cut(Image srcImage, int x, int y, int radius) {
+ return Img.from(srcImage).cut(x, y, radius).getImg();
+ }
+
+ /**
+ * 图像切片(指定切片的宽度和高度)
+ *
+ * @param srcImageFile 源图像
+ * @param descDir 切片目标文件夹
+ * @param destWidth 目标切片宽度。默认200
+ * @param destHeight 目标切片高度。默认150
+ */
+ public static void slice(File srcImageFile, File descDir, int destWidth, int destHeight) {
+ BufferedImage image = null;
+ try {
+ image = read(srcImageFile);
+ slice(image, descDir, destWidth, destHeight);
+ } finally {
+ flush(image);
+ }
+ }
+
+ /**
+ * 图像切片(指定切片的宽度和高度)
+ *
+ * @param srcImage 源图像,使用结束后需手动调用{@link #flush(Image)}释放资源
+ * @param descDir 切片目标文件夹
+ * @param destWidth 目标切片宽度。默认200
+ * @param destHeight 目标切片高度。默认150
+ */
+ public static void slice(Image srcImage, File descDir, int destWidth, int destHeight) {
+ if (destWidth <= 0) {
+ destWidth = 200; // 切片宽度
+ }
+ if (destHeight <= 0) {
+ destHeight = 150; // 切片高度
+ }
+ int srcWidth = srcImage.getWidth(null); // 源图宽度
+ int srcHeight = srcImage.getHeight(null); // 源图高度
+
+ if (srcWidth < destWidth) {
+ destWidth = srcWidth;
+ }
+ if (srcHeight < destHeight) {
+ destHeight = srcHeight;
+ }
+
+ int cols; // 切片横向数量
+ int rows; // 切片纵向数量
+ // 计算切片的横向和纵向数量
+ if (srcWidth % destWidth == 0) {
+ cols = srcWidth / destWidth;
+ } else {
+ cols = (int)Math.floor((double)srcWidth / destWidth) + 1;
+ }
+ if (srcHeight % destHeight == 0) {
+ rows = srcHeight / destHeight;
+ } else {
+ rows = (int)Math.floor((double)srcHeight / destHeight) + 1;
+ }
+ // 循环建立切片
+ Image tag;
+ for (int i = 0; i < rows; i++) {
+ for (int j = 0; j < cols; j++) {
+ // 四个参数分别为图像起点坐标和宽高
+ // 即: CropImageFilter(int x,int y,int width,int height)
+ tag = cut(srcImage, new Rectangle(j * destWidth, i * destHeight, destWidth, destHeight));
+ // 输出为文件
+ write(tag, FileTools.file(descDir, "_r" + i + "_c" + j + ".jpg"));
+ }
+ }
+ }
+
+ /**
+ * 图像切割(指定切片的行数和列数)
+ *
+ * @param srcImageFile 源图像文件
+ * @param destDir 切片目标文件夹
+ * @param rows 目标切片行数。默认2,必须是范围 [1, 20] 之内
+ * @param cols 目标切片列数。默认2,必须是范围 [1, 20] 之内
+ */
+ public static void sliceByRowsAndCols(File srcImageFile, File destDir, int rows, int cols) {
+ sliceByRowsAndCols(srcImageFile, destDir, IMAGE_TYPE_JPEG, rows, cols);
+ }
+
+ /**
+ * 图像切割(指定切片的行数和列数)
+ *
+ * @param srcImageFile 源图像文件
+ * @param destDir 切片目标文件夹
+ * @param format 目标文件格式
+ * @param rows 目标切片行数。默认2,必须是范围 [1, 20] 之内
+ * @param cols 目标切片列数。默认2,必须是范围 [1, 20] 之内
+ */
+ public static void sliceByRowsAndCols(File srcImageFile, File destDir, String format, int rows, int cols) {
+ BufferedImage image = null;
+ try {
+ image = read(srcImageFile);
+ sliceByRowsAndCols(image, destDir, format, rows, cols);
+ } finally {
+ flush(image);
+ }
+ }
+
+ /**
+ * 图像切割(指定切片的行数和列数),默认RGB模式
+ *
+ * @param srcImage 源图像,如果非{@link BufferedImage},则默认使用RGB模式
+ * @param destDir 切片目标文件夹
+ * @param rows 目标切片行数。默认2,必须是范围 [1, 20] 之内
+ * @param cols 目标切片列数。默认2,必须是范围 [1, 20] 之内
+ */
+ public static void sliceByRowsAndCols(Image srcImage, File destDir, int rows, int cols) {
+ sliceByRowsAndCols(srcImage, destDir, IMAGE_TYPE_JPEG, rows, cols);
+ }
+
+ /**
+ * 图像切割(指定切片的行数和列数),默认RGB模式
+ *
+ * @param srcImage 源图像,如果非{@link BufferedImage},则默认使用RGB模式
+ * @param destDir 切片目标文件夹
+ * @param format 目标文件格式
+ * @param rows 目标切片行数。默认2,必须是范围 [1, 20] 之内
+ * @param cols 目标切片列数。默认2,必须是范围 [1, 20] 之内
+ * @since 5.8.6
+ */
+ public static void sliceByRowsAndCols(Image srcImage, File destDir, String format, int rows, int cols) {
+ if (!destDir.exists()) {
+ FileTools.mkdir(destDir);
+ } else if (!destDir.isDirectory()) {
+ throw new IllegalArgumentException("Destination Dir must be a Directory !");
+ }
+
+ if (rows <= 0 || rows > 20) {
+ rows = 2; // 切片行数
+ }
+ if (cols <= 0 || cols > 20) {
+ cols = 2; // 切片列数
+ }
+ // 读取源图像
+ int srcWidth = srcImage.getWidth(null); // 源图宽度
+ int srcHeight = srcImage.getHeight(null); // 源图高度
+
+ int destWidth = NumberUtil.partValue(srcWidth, cols); // 每张切片的宽度
+ int destHeight = NumberUtil.partValue(srcHeight, rows); // 每张切片的高度
+
+ // 循环建立切片
+ Image tag;
+ for (int i = 0; i < rows; i++) {
+ for (int j = 0; j < cols; j++) {
+ tag = cut(srcImage, new Rectangle(j * destWidth, i * destHeight, destWidth, destHeight));
+ // 输出为文件
+ write(tag, new File(destDir, "_r" + i + "_c" + j + "." + format));
+ }
+ }
+ }
+
+ // ----------------------------------------------------------------------------------------------------------------------
+ // convert
+
+ /**
+ * 图像类型转换:GIF=》JPG、GIF=》PNG、PNG=》JPG、PNG=》GIF(X)、BMP=》PNG
+ *
+ * @param srcImageFile 源图像文件
+ * @param destImageFile 目标图像文件
+ */
+ public static void convert(File srcImageFile, File destImageFile) {
+ Assert.notNull(srcImageFile);
+ Assert.notNull(destImageFile);
+ Assert.isTrue(!srcImageFile.equals(destImageFile), "Src file is equals to dest file!");
+
+ final String srcExtName = FileNameUtil.extName(srcImageFile);
+ final String destExtName = FileNameUtil.extName(destImageFile);
+ if (StringTools.equalsIgnoreCase(srcExtName, destExtName)) {
+ // 扩展名相同直接复制文件
+ try {
+ FileTools.copy(srcImageFile, destImageFile, true, false);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ Img img = null;
+ try {
+ img = Img.from(srcImageFile);
+ img.write(destImageFile);
+ } finally {
+ IoUtil.flush(img);
+ }
+ }
+
+ /**
+ * 图像类型转换:GIF=》JPG、GIF=》PNG、PNG=》JPG、PNG=》GIF(X)、BMP=》PNG
+ * 此方法并不关闭流
+ *
+ * @param srcStream 源图像流
+ * @param formatName 包含格式非正式名称的 String:如JPG、JPEG、GIF等
+ * @param destStream 目标图像输出流
+ * @since 3.0.9
+ */
+ public static void convert(InputStream srcStream, String formatName, OutputStream destStream) {
+ BufferedImage image = null;
+ try {
+ image = read(srcStream);
+ write(image, formatName, getImageOutputStream(destStream));
+ } finally {
+ flush(image);
+ }
+ }
+
+ /**
+ * 图像类型转换:GIF=》JPG、GIF=》PNG、PNG=》JPG、PNG=》GIF(X)、BMP=》PNG
+ * 此方法并不关闭流
+ *
+ * @param srcImage 源图像流,使用结束后需手动调用{@link #flush(Image)}释放资源
+ * @param formatName 包含格式非正式名称的 String:如JPG、JPEG、GIF等
+ * @param destImageStream 目标图像输出流
+ * @since 4.1.14
+ */
+ public static void convert(Image srcImage, String formatName, ImageOutputStream destImageStream) {
+ Img.from(srcImage).setTargetImageType(formatName).write(destImageStream);
+ }
+
+ /**
+ * 图像类型转换:GIF=》JPG、GIF=》PNG、PNG=》JPG、PNG=》GIF(X)、BMP=》PNG
+ * 此方法并不关闭流
+ *
+ * @param srcImage 源图像流,使用结束后需手动调用{@link #flush(Image)}释放资源
+ * @param formatName 包含格式非正式名称的 String:如JPG、JPEG、GIF等
+ * @param destImageStream 目标图像输出流
+ * @param isSrcPng 源图片是否为PNG格式(参数无效)
+ * @since 4.1.14
+ */
+ @Deprecated
+ public static void convert(Image srcImage, String formatName, ImageOutputStream destImageStream, boolean isSrcPng) {
+ convert(srcImage, formatName, destImageStream);
+ }
+
+ // ----------------------------------------------------------------------------------------------------------------------
+ // grey
+
+ /**
+ * 彩色转为黑白
+ *
+ * @param srcImageFile 源图像地址
+ * @param destImageFile 目标图像地址
+ */
+ public static void gray(File srcImageFile, File destImageFile) {
+ BufferedImage image = null;
+ try {
+ image = read(srcImageFile);
+ gray(image, destImageFile);
+ } finally {
+ flush(image);
+ }
+ }
+
+ /**
+ * 彩色转为黑白
+ * 此方法并不关闭流
+ *
+ * @param srcStream 源图像流
+ * @param destStream 目标图像流
+ * @since 3.0.9
+ */
+ public static void gray(InputStream srcStream, OutputStream destStream) {
+ BufferedImage image = null;
+ try {
+ image = read(srcStream);
+ gray(image, destStream);
+ } finally {
+ flush(image);
+ }
+ }
+
+ /**
+ * 彩色转为黑白
+ * 此方法并不关闭流
+ *
+ * @param srcStream 源图像流
+ * @param destStream 目标图像流
+ * @since 3.0.9
+ */
+ public static void gray(ImageInputStream srcStream, ImageOutputStream destStream) {
+ BufferedImage image = null;
+ try {
+ image = read(srcStream);
+ gray(image, destStream);
+ } finally {
+ flush(image);
+ }
+ }
+
+ /**
+ * 彩色转为黑白
+ *
+ * @param srcImage 源图像流,使用结束后需手动调用{@link #flush(Image)}释放资源
+ * @param outFile 目标文件
+ * @since 3.2.2
+ */
+ public static void gray(Image srcImage, File outFile) {
+ write(gray(srcImage), outFile);
+ }
+
+ /**
+ * 彩色转为黑白
+ * 此方法并不关闭流
+ *
+ * @param srcImage 源图像流,使用结束后需手动调用{@link #flush(Image)}释放资源
+ * @param out 目标图像流
+ * @since 3.2.2
+ */
+ public static void gray(Image srcImage, OutputStream out) {
+ gray(srcImage, getImageOutputStream(out));
+ }
+
+ /**
+ * 彩色转为黑白
+ * 此方法并不关闭流
+ *
+ * @param srcImage 源图像流,使用结束后需手动调用{@link #flush(Image)}释放资源
+ * @param destImageStream 目标图像流
+ * @throws RuntimeException IO异常
+ * @since 3.0.9
+ */
+ public static void gray(Image srcImage, ImageOutputStream destImageStream) throws RuntimeException {
+ writeJpg(gray(srcImage), destImageStream);
+ }
+
+ /**
+ * 彩色转为黑白
+ *
+ * @param srcImage 源图像流,使用结束后需手动调用{@link #flush(Image)}释放资源
+ * @return {@link Image}灰度后的图片
+ * @since 3.1.0
+ */
+ public static Image gray(Image srcImage) {
+ return Img.from(srcImage).gray().getImg();
+ }
+
+ // ----------------------------------------------------------------------------------------------------------------------
+ // binary
+
+ /**
+ * 彩色转为黑白二值化图片,根据目标文件扩展名确定转换后的格式
+ *
+ * @param srcImageFile 源图像地址
+ * @param destImageFile 目标图像地址
+ */
+ public static void binary(File srcImageFile, File destImageFile) {
+ BufferedImage image = null;
+ try {
+ image = read(srcImageFile);
+ binary(image, destImageFile);
+ } finally {
+ flush(image);
+ }
+ }
+
+ /**
+ * 彩色转为黑白二值化图片
+ * 此方法并不关闭流
+ *
+ * @param srcStream 源图像流
+ * @param destStream 目标图像流
+ * @param imageType 图片格式(扩展名)
+ * @since 4.0.5
+ */
+ public static void binary(InputStream srcStream, OutputStream destStream, String imageType) {
+ BufferedImage image = null;
+ try {
+ image = read(srcStream);
+ binary(image, getImageOutputStream(destStream), imageType);
+ } finally {
+ flush(image);
+ }
+ }
+
+ /**
+ * 彩色转为黑白黑白二值化图片
+ * 此方法并不关闭流
+ *
+ * @param srcStream 源图像流
+ * @param destStream 目标图像流
+ * @param imageType 图片格式(扩展名)
+ * @since 4.0.5
+ */
+ public static void binary(ImageInputStream srcStream, ImageOutputStream destStream, String imageType) {
+ BufferedImage image = null;
+ try {
+ image = read(srcStream);
+ binary(image, destStream, imageType);
+ } finally {
+ flush(image);
+ }
+ }
+
+ /**
+ * 彩色转为黑白二值化图片,根据目标文件扩展名确定转换后的格式
+ *
+ * @param srcImage 源图像,使用结束后需手动调用{@link #flush(Image)}释放资源
+ * @param outFile 目标文件
+ * @since 4.0.5
+ */
+ public static void binary(Image srcImage, File outFile) {
+ write(binary(srcImage), outFile);
+ }
+
+ /**
+ * 彩色转为黑白二值化图片
+ * 此方法并不关闭流,输出JPG格式
+ *
+ * @param srcImage 源图像,使用结束后需手动调用{@link #flush(Image)}释放资源
+ * @param out 目标图像流
+ * @param imageType 图片格式(扩展名)
+ * @since 4.0.5
+ */
+ public static void binary(Image srcImage, OutputStream out, String imageType) {
+ binary(srcImage, getImageOutputStream(out), imageType);
+ }
+
+ /**
+ * 彩色转为黑白二值化图片
+ * 此方法并不关闭流,输出JPG格式
+ *
+ * @param srcImage 源图像,使用结束后需手动调用{@link #flush(Image)}释放资源
+ * @param destImageStream 目标图像流
+ * @param imageType 图片格式(扩展名)
+ * @throws RuntimeException IO异常
+ * @since 4.0.5
+ */
+ public static void binary(Image srcImage, ImageOutputStream destImageStream, String imageType) throws RuntimeException {
+ write(binary(srcImage), imageType, destImageStream);
+ }
+
+ /**
+ * 彩色转为黑白二值化图片
+ *
+ * @param srcImage 源图像,使用结束后需手动调用{@link #flush(Image)}释放资源
+ * @return {@link Image}二值化后的图片
+ * @since 4.0.5
+ */
+ public static Image binary(Image srcImage) {
+ return Img.from(srcImage).binary().getImg();
+ }
+
+ // ----------------------------------------------------------------------------------------------------------------------
+ // press
+
+ /**
+ * 给图片添加文字水印
+ *
+ * @param imageFile 源图像文件
+ * @param destFile 目标图像文件
+ * @param pressText 水印文字
+ * @param color 水印的字体颜色
+ * @param font {@link Font} 字体相关信息,如果默认则为{@code null}
+ * @param x 修正值。 默认在中间,偏移量相对于中间偏移
+ * @param y 修正值。 默认在中间,偏移量相对于中间偏移
+ * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字
+ */
+ public static void pressText(File imageFile, File destFile, String pressText, Color color, Font font, int x, int y, float alpha) {
+ BufferedImage image = null;
+ try {
+ image = read(imageFile);
+ pressText(image, destFile, pressText, color, font, x, y, alpha);
+ } finally {
+ flush(image);
+ }
+ }
+
+ /**
+ * 给图片添加文字水印
+ * 此方法并不关闭流
+ *
+ * @param srcStream 源图像流
+ * @param destStream 目标图像流
+ * @param pressText 水印文字
+ * @param color 水印的字体颜色
+ * @param font {@link Font} 字体相关信息,如果默认则为{@code null}
+ * @param x 修正值。 默认在中间,偏移量相对于中间偏移
+ * @param y 修正值。 默认在中间,偏移量相对于中间偏移
+ * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字
+ */
+ public static void pressText(InputStream srcStream, OutputStream destStream, String pressText, Color color, Font font, int x, int y,
+ float alpha) {
+ BufferedImage image = null;
+ try {
+ image = read(srcStream);
+ pressText(image, getImageOutputStream(destStream), pressText, color, font, x, y, alpha);
+ } finally {
+ flush(image);
+ }
+ }
+
+ /**
+ * 给图片添加文字水印
+ * 此方法并不关闭流
+ *
+ * @param srcStream 源图像流
+ * @param destStream 目标图像流
+ * @param pressText 水印文字
+ * @param color 水印的字体颜色
+ * @param font {@link Font} 字体相关信息,如果默认则为{@code null}
+ * @param x 修正值。 默认在中间,偏移量相对于中间偏移
+ * @param y 修正值。 默认在中间,偏移量相对于中间偏移
+ * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字
+ */
+ public static void pressText(ImageInputStream srcStream, ImageOutputStream destStream, String pressText, Color color, Font font, int x, int y,
+ float alpha) {
+ BufferedImage image = null;
+ try {
+ image = read(srcStream);
+ pressText(image, destStream, pressText, color, font, x, y, alpha);
+ } finally {
+ flush(image);
+ }
+ }
+
+ /**
+ * 给图片添加文字水印
+ * 此方法并不关闭流
+ *
+ * @param srcImage 源图像,使用结束后需手动调用{@link #flush(Image)}释放资源
+ * @param destFile 目标流
+ * @param pressText 水印文字
+ * @param color 水印的字体颜色
+ * @param font {@link Font} 字体相关信息,如果默认则为{@code null}
+ * @param x 修正值。 默认在中间,偏移量相对于中间偏移
+ * @param y 修正值。 默认在中间,偏移量相对于中间偏移
+ * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字
+ * @throws RuntimeException IO异常
+ * @since 3.2.2
+ */
+ public static void pressText(Image srcImage, File destFile, String pressText, Color color, Font font, int x, int y, float alpha)
+ throws RuntimeException {
+ write(pressText(srcImage, pressText, color, font, x, y, alpha), destFile);
+ }
+
+ /**
+ * 给图片添加文字水印
+ * 此方法并不关闭流
+ *
+ * @param srcImage 源图像,使用结束后需手动调用{@link #flush(Image)}释放资源
+ * @param to 目标流
+ * @param pressText 水印文字
+ * @param color 水印的字体颜色
+ * @param font {@link Font} 字体相关信息,如果默认则为{@code null}
+ * @param x 修正值。 默认在中间,偏移量相对于中间偏移
+ * @param y 修正值。 默认在中间,偏移量相对于中间偏移
+ * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字
+ * @throws RuntimeException IO异常
+ * @since 3.2.2
+ */
+ public static void pressText(Image srcImage, OutputStream to, String pressText, Color color, Font font, int x, int y, float alpha)
+ throws RuntimeException {
+ pressText(srcImage, getImageOutputStream(to), pressText, color, font, x, y, alpha);
+ }
+
+ /**
+ * 给图片添加文字水印
+ * 此方法并不关闭流
+ *
+ * @param srcImage 源图像,使用结束后需手动调用{@link #flush(Image)}释放资源
+ * @param destImageStream 目标图像流
+ * @param pressText 水印文字
+ * @param color 水印的字体颜色
+ * @param font {@link Font} 字体相关信息,如果默认则为{@code null}
+ * @param x 修正值。 默认在中间,偏移量相对于中间偏移
+ * @param y 修正值。 默认在中间,偏移量相对于中间偏移
+ * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字
+ * @throws RuntimeException IO异常
+ */
+ public static void pressText(Image srcImage, ImageOutputStream destImageStream, String pressText, Color color, Font font, int x, int y,
+ float alpha) throws RuntimeException {
+ writeJpg(pressText(srcImage, pressText, color, font, x, y, alpha), destImageStream);
+ }
+
+ /**
+ * 给图片添加文字水印
+ * 此方法并不关闭流
+ *
+ * @param srcImage 源图像,使用结束后需手动调用{@link #flush(Image)}释放资源
+ * @param pressText 水印文字
+ * @param color 水印的字体颜色
+ * @param font {@link Font} 字体相关信息,如果默认则为{@code null}
+ * @param x 修正值。 默认在中间,偏移量相对于中间偏移
+ * @param y 修正值。 默认在中间,偏移量相对于中间偏移
+ * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字
+ * @return 处理后的图像
+ * @since 3.2.2
+ */
+ public static Image pressText(Image srcImage, String pressText, Color color, Font font, int x, int y, float alpha) {
+ return Img.from(srcImage).pressText(pressText, color, font, x, y, alpha).getImg();
+ }
+
+ /**
+ * 给图片添加图片水印
+ *
+ * @param srcImageFile 源图像文件
+ * @param destImageFile 目标图像文件
+ * @param pressImg 水印图片
+ * @param x 修正值。 默认在中间,偏移量相对于中间偏移
+ * @param y 修正值。 默认在中间,偏移量相对于中间偏移
+ * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字
+ */
+ public static void pressImage(File srcImageFile, File destImageFile, Image pressImg, int x, int y, float alpha) {
+ BufferedImage image = null;
+ try {
+ image = read(srcImageFile);
+ pressImage(image, destImageFile, pressImg, x, y, alpha);
+ } finally {
+ flush(image);
+ }
+ }
+
+ /**
+ * 给图片添加图片水印
+ * 此方法并不关闭流
+ *
+ * @param srcStream 源图像流
+ * @param destStream 目标图像流
+ * @param pressImg 水印图片,可以使用{@link ImageIO#read(File)}方法读取文件
+ * @param x 修正值。 默认在中间,偏移量相对于中间偏移
+ * @param y 修正值。 默认在中间,偏移量相对于中间偏移
+ * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字
+ */
+ public static void pressImage(InputStream srcStream, OutputStream destStream, Image pressImg, int x, int y, float alpha) {
+ BufferedImage image = null;
+ try {
+ image = read(srcStream);
+ pressImage(image, getImageOutputStream(destStream), pressImg, x, y, alpha);
+ } finally {
+ flush(image);
+ }
+ }
+
+ /**
+ * 给图片添加图片水印
+ * 此方法并不关闭流
+ *
+ * @param srcStream 源图像流
+ * @param destStream 目标图像流
+ * @param pressImg 水印图片,可以使用{@link ImageIO#read(File)}方法读取文件
+ * @param x 修正值。 默认在中间,偏移量相对于中间偏移
+ * @param y 修正值。 默认在中间,偏移量相对于中间偏移
+ * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字
+ * @throws RuntimeException IO异常
+ */
+ public static void pressImage(ImageInputStream srcStream, ImageOutputStream destStream, Image pressImg, int x, int y, float alpha)
+ throws RuntimeException {
+ BufferedImage image = null;
+ try {
+ image = read(srcStream);
+ pressImage(image, destStream, pressImg, x, y, alpha);
+ } finally {
+ flush(image);
+ }
+
+ }
+
+ /**
+ * 给图片添加图片水印
+ * 此方法并不关闭流
+ *
+ * @param srcImage 源图像流,使用结束后需手动调用{@link #flush(Image)}释放资源
+ * @param outFile 写出文件
+ * @param pressImg 水印图片,可以使用{@link ImageIO#read(File)}方法读取文件
+ * @param x 修正值。 默认在中间,偏移量相对于中间偏移
+ * @param y 修正值。 默认在中间,偏移量相对于中间偏移
+ * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字
+ * @throws RuntimeException IO异常
+ * @since 3.2.2
+ */
+ public static void pressImage(Image srcImage, File outFile, Image pressImg, int x, int y, float alpha) throws RuntimeException {
+ write(pressImage(srcImage, pressImg, x, y, alpha), outFile);
+ }
+
+ /**
+ * 给图片添加图片水印
+ * 此方法并不关闭流
+ *
+ * @param srcImage 源图像流,使用结束后需手动调用{@link #flush(Image)}释放资源
+ * @param out 目标图像流
+ * @param pressImg 水印图片,可以使用{@link ImageIO#read(File)}方法读取文件
+ * @param x 修正值。 默认在中间,偏移量相对于中间偏移
+ * @param y 修正值。 默认在中间,偏移量相对于中间偏移
+ * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字
+ * @throws RuntimeException IO异常
+ * @since 3.2.2
+ */
+ public static void pressImage(Image srcImage, OutputStream out, Image pressImg, int x, int y, float alpha) throws RuntimeException {
+ pressImage(srcImage, getImageOutputStream(out), pressImg, x, y, alpha);
+ }
+
+ /**
+ * 给图片添加图片水印
+ * 此方法并不关闭流
+ *
+ * @param srcImage 源图像流,使用结束后需手动调用{@link #flush(Image)}释放资源
+ * @param destImageStream 目标图像流
+ * @param pressImg 水印图片,可以使用{@link ImageIO#read(File)}方法读取文件
+ * @param x 修正值。 默认在中间,偏移量相对于中间偏移
+ * @param y 修正值。 默认在中间,偏移量相对于中间偏移
+ * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字
+ * @throws RuntimeException IO异常
+ */
+ public static void pressImage(Image srcImage, ImageOutputStream destImageStream, Image pressImg, int x, int y, float alpha)
+ throws RuntimeException {
+ writeJpg(pressImage(srcImage, pressImg, x, y, alpha), destImageStream);
+ }
+
+ /**
+ * 给图片添加图片水印
+ * 此方法并不关闭流
+ *
+ * @param srcImage 源图像流,使用结束后需手动调用{@link #flush(Image)}释放资源
+ * @param pressImg 水印图片,可以使用{@link ImageIO#read(File)}方法读取文件
+ * @param x 修正值。 默认在中间,偏移量相对于中间偏移
+ * @param y 修正值。 默认在中间,偏移量相对于中间偏移
+ * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字
+ * @return 结果图片
+ */
+ public static Image pressImage(Image srcImage, Image pressImg, int x, int y, float alpha) {
+ return Img.from(srcImage).pressImage(pressImg, x, y, alpha).getImg();
+ }
+
+ /**
+ * 给图片添加图片水印
+ * 此方法并不关闭流
+ *
+ * @param srcImage 源图像流,使用结束后需手动调用{@link #flush(Image)}释放资源
+ * @param pressImg 水印图片,可以使用{@link ImageIO#read(File)}方法读取文件
+ * @param rectangle 矩形对象,表示矩形区域的x,y,width,height,x,y从背景图片中心计算
+ * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字
+ * @return 结果图片
+ * @since 4.1.14
+ */
+ public static Image pressImage(Image srcImage, Image pressImg, Rectangle rectangle, float alpha) {
+ return Img.from(srcImage).pressImage(pressImg, rectangle, alpha).getImg();
+ }
+
+ // ----------------------------------------------------------------------------------------------------------------------
+ // rotate
+
+ /**
+ * 旋转图片为指定角度
+ * 此方法不会关闭输出流
+ *
+ * @param imageFile 被旋转图像文件
+ * @param degree 旋转角度
+ * @param outFile 输出文件
+ * @throws RuntimeException IO异常
+ * @since 3.2.2
+ */
+ public static void rotate(File imageFile, int degree, File outFile) throws RuntimeException {
+ BufferedImage image = null;
+ try {
+ image = read(imageFile);
+ rotate(image, degree, outFile);
+ } finally {
+ flush(image);
+ }
+ }
+
+ /**
+ * 旋转图片为指定角度
+ * 此方法不会关闭输出流
+ *
+ * @param image 目标图像,使用结束后需手动调用{@link #flush(Image)}释放资源
+ * @param degree 旋转角度
+ * @param outFile 输出文件
+ * @throws RuntimeException IO异常
+ * @since 3.2.2
+ */
+ public static void rotate(Image image, int degree, File outFile) throws RuntimeException {
+ write(rotate(image, degree), outFile);
+ }
+
+ /**
+ * 旋转图片为指定角度
+ * 此方法不会关闭输出流
+ *
+ * @param image 目标图像,使用结束后需手动调用{@link #flush(Image)}释放资源
+ * @param degree 旋转角度
+ * @param out 输出流
+ * @throws RuntimeException IO异常
+ * @since 3.2.2
+ */
+ public static void rotate(Image image, int degree, OutputStream out) throws RuntimeException {
+ writeJpg(rotate(image, degree), getImageOutputStream(out));
+ }
+
+ /**
+ * 旋转图片为指定角度
+ * 此方法不会关闭输出流,输出格式为JPG
+ *
+ * @param image 图像,使用结束后需手动调用{@link #flush(Image)}释放资源
+ * @param degree 旋转角度
+ * @param out 输出图像流
+ * @throws RuntimeException IO异常
+ * @since 3.2.2
+ */
+ public static void rotate(Image image, int degree, ImageOutputStream out) throws RuntimeException {
+ writeJpg(rotate(image, degree), out);
+ }
+
+ /**
+ * 旋转图片为指定角度
+ * 来自:http://blog.51cto.com/cping1982/130066
+ *
+ * @param image 图像,使用结束后需手动调用{@link #flush(Image)}释放资源
+ * @param degree 旋转角度
+ * @return 旋转后的图片
+ * @since 3.2.2
+ */
+ public static Image rotate(Image image, int degree) {
+ return Img.from(image).rotate(degree).getImg();
+ }
+
+ // ----------------------------------------------------------------------------------------------------------------------
+ // flip
+
+ /**
+ * 水平翻转图像
+ *
+ * @param imageFile 图像文件
+ * @param outFile 输出文件
+ * @throws RuntimeException IO异常
+ * @since 3.2.2
+ */
+ public static void flip(File imageFile, File outFile) throws RuntimeException {
+ BufferedImage image = null;
+ try {
+ image = read(imageFile);
+ flip(image, outFile);
+ } finally {
+ flush(image);
+ }
+ }
+
+ /**
+ * 水平翻转图像
+ *
+ * @param image 图像,使用结束后需手动调用{@link #flush(Image)}释放资源
+ * @param outFile 输出文件
+ * @throws RuntimeException IO异常
+ * @since 3.2.2
+ */
+ public static void flip(Image image, File outFile) throws RuntimeException {
+ write(flip(image), outFile);
+ }
+
+ /**
+ * 水平翻转图像
+ *
+ * @param image 图像,使用结束后需手动调用{@link #flush(Image)}释放资源
+ * @param out 输出
+ * @throws RuntimeException IO异常
+ * @since 3.2.2
+ */
+ public static void flip(Image image, OutputStream out) throws RuntimeException {
+ flip(image, getImageOutputStream(out));
+ }
+
+ /**
+ * 水平翻转图像,写出格式为JPG
+ *
+ * @param image 图像,使用结束后需手动调用{@link #flush(Image)}释放资源
+ * @param out 输出
+ * @throws RuntimeException IO异常
+ * @since 3.2.2
+ */
+ public static void flip(Image image, ImageOutputStream out) throws RuntimeException {
+ writeJpg(flip(image), out);
+ }
+
+ /**
+ * 水平翻转图像
+ *
+ * @param image 图像,使用结束后需手动调用{@link #flush(Image)}释放资源
+ * @return 翻转后的图片
+ * @since 3.2.2
+ */
+ public static Image flip(Image image) {
+ return Img.from(image).flip().getImg();
+ }
+
+ // ----------------------------------------------------------------------------------------------------------------------
+ // compress
+
+ /**
+ * 压缩图像,输出图像只支持jpg文件
+ *
+ * @param imageFile 图像文件
+ * @param outFile 输出文件,只支持jpg文件
+ * @param quality 压缩比例,必须为0~1
+ * @throws RuntimeException IO异常
+ * @since 4.3.2
+ */
+ public static void compress(File imageFile, File outFile, float quality) throws RuntimeException {
+ Img img = null;
+ try {
+ img = Img.from(imageFile);
+ img.setQuality(quality).write(outFile);
+ } finally {
+ IoUtil.flush(img);
+ }
+ }
+
+ // ----------------------------------------------------------------------------------------------------------------------
+ // other
+
+ /**
+ * {@link Image} 转 {@link RenderedImage}
+ * 首先尝试强转,否则新建一个{@link BufferedImage}后重新绘制,使用 {@link BufferedImage#TYPE_INT_RGB} 模式。
+ *
+ * @param img {@link Image}
+ * @return {@link BufferedImage}
+ * @since 4.3.2
+ * @deprecated 改用 {@link #castToRenderedImage(Image, String)}
+ */
+ @Deprecated
+ public static RenderedImage toRenderedImage(Image img) {
+ return castToRenderedImage(img, IMAGE_TYPE_JPG);
+ }
+
+ /**
+ * {@link Image} 转 {@link BufferedImage}
+ * 首先尝试强转,否则新建一个{@link BufferedImage}后重新绘制,使用 {@link BufferedImage#TYPE_INT_RGB} 模式
+ *
+ * @param img {@link Image}
+ * @return {@link BufferedImage}
+ * @deprecated 改用 {@link #castToBufferedImage(Image, String)}
+ */
+ @Deprecated
+ public static BufferedImage toBufferedImage(Image img) {
+ return castToBufferedImage(img, IMAGE_TYPE_JPG);
+ }
+
+ /**
+ * {@link Image} 转 {@link RenderedImage}
+ * 首先尝试强转,否则新建一个{@link BufferedImage}后重新绘制,使用 {@link BufferedImage#TYPE_INT_RGB} 模式。
+ *
+ * @param img {@link Image}
+ * @param imageType 目标图片类型,例如jpg或png等
+ * @return {@link BufferedImage}
+ * @since 4.3.2
+ */
+ public static RenderedImage castToRenderedImage(final Image img, final String imageType) {
+ if (img instanceof RenderedImage) {
+ return (RenderedImage)img;
+ }
+
+ return toBufferedImage(img, imageType);
+ }
+
+ /**
+ * {@link Image} 转 {@link BufferedImage}
+ * 首先尝试强转,否则新建一个{@link BufferedImage}后重新绘制,使用 imageType 模式
+ *
+ * @param img {@link Image}
+ * @param imageType 目标图片类型,例如jpg或png等
+ * @return {@link BufferedImage}
+ */
+ public static BufferedImage castToBufferedImage(final Image img, final String imageType) {
+ if (img instanceof BufferedImage) {
+ return (BufferedImage)img;
+ }
+
+ return toBufferedImage(img, imageType);
+ }
+
+ /**
+ * {@link Image} 转 {@link BufferedImage}
+ * 如果源图片的RGB模式与目标模式一致,则直接转换,否则重新绘制
+ * 默认的,png图片使用 {@link BufferedImage#TYPE_INT_ARGB}模式,其它使用 {@link BufferedImage#TYPE_INT_RGB} 模式
+ *
+ * @param image {@link Image}
+ * @param imageType 目标图片类型,例如jpg或png等
+ * @return {@link BufferedImage}
+ * @since 4.3.2
+ */
+ public static BufferedImage toBufferedImage(Image image, String imageType) {
+ return toBufferedImage(image, imageType, null);
+ }
+
+ /**
+ * {@link Image} 转 {@link BufferedImage}
+ * 如果源图片的RGB模式与目标模式一致,则直接转换,否则重新绘制
+ * 默认的,png图片使用 {@link BufferedImage#TYPE_INT_ARGB}模式,其它使用 {@link BufferedImage#TYPE_INT_RGB} 模式
+ *
+ * @param image {@link Image}
+ * @param imageType 目标图片类型,例如jpg或png等
+ * @param backgroundColor 背景色{@link Color}
+ * @return {@link BufferedImage}
+ * @since 4.3.2
+ */
+ public static BufferedImage toBufferedImage(Image image, String imageType, Color backgroundColor) {
+ final int type = IMAGE_TYPE_PNG.equalsIgnoreCase(imageType)
+ ? BufferedImage.TYPE_INT_ARGB
+ : BufferedImage.TYPE_INT_RGB;
+ return toBufferedImage(image, type, backgroundColor);
+ }
+
+ /**
+ * {@link Image} 转 {@link BufferedImage}
+ * 如果源图片的RGB模式与目标模式一致,则直接转换,否则重新绘制
+ *
+ * @param image {@link Image}
+ * @param imageType 目标图片类型,{@link BufferedImage}中的常量,例如黑白等
+ * @return {@link BufferedImage}
+ * @since 5.4.7
+ */
+ public static BufferedImage toBufferedImage(Image image, int imageType) {
+ BufferedImage bufferedImage;
+ if (image instanceof BufferedImage) {
+ bufferedImage = (BufferedImage)image;
+ if (imageType != bufferedImage.getType()) {
+ bufferedImage = copyImage(image, imageType);
+ }
+ return bufferedImage;
+ }
+
+ bufferedImage = copyImage(image, imageType);
+ return bufferedImage;
+ }
+
+ /**
+ * {@link Image} 转 {@link BufferedImage}
+ * 如果源图片的RGB模式与目标模式一致,则直接转换,否则重新绘制
+ *
+ * @param image {@link Image}
+ * @param imageType 目标图片类型,{@link BufferedImage}中的常量,例如黑白等
+ * @param backgroundColor 背景色{@link Color}
+ * @return {@link BufferedImage}
+ * @since 5.4.7
+ */
+ public static BufferedImage toBufferedImage(Image image, int imageType, Color backgroundColor) {
+ BufferedImage bufferedImage;
+ if (image instanceof BufferedImage) {
+ bufferedImage = (BufferedImage)image;
+ if (imageType != bufferedImage.getType()) {
+ bufferedImage = copyImage(image, imageType, backgroundColor);
+ }
+ return bufferedImage;
+ }
+
+ bufferedImage = copyImage(image, imageType, backgroundColor);
+ return bufferedImage;
+ }
+
+ /**
+ * 将已有Image复制新的一份出来
+ *
+ * @param img {@link Image}
+ * @param imageType 目标图片类型,{@link BufferedImage}中的常量,例如黑白等
+ * @return {@link BufferedImage}
+ * @see BufferedImage#TYPE_INT_RGB
+ * @see BufferedImage#TYPE_INT_ARGB
+ * @see BufferedImage#TYPE_INT_ARGB_PRE
+ * @see BufferedImage#TYPE_INT_BGR
+ * @see BufferedImage#TYPE_3BYTE_BGR
+ * @see BufferedImage#TYPE_4BYTE_ABGR
+ * @see BufferedImage#TYPE_4BYTE_ABGR_PRE
+ * @see BufferedImage#TYPE_BYTE_GRAY
+ * @see BufferedImage#TYPE_USHORT_GRAY
+ * @see BufferedImage#TYPE_BYTE_BINARY
+ * @see BufferedImage#TYPE_BYTE_INDEXED
+ * @see BufferedImage#TYPE_USHORT_565_RGB
+ * @see BufferedImage#TYPE_USHORT_555_RGB
+ */
+ public static BufferedImage copyImage(Image img, int imageType) {
+ return copyImage(img, imageType, null);
+ }
+
+ /**
+ * 将已有Image复制新的一份出来
+ *
+ * @param img {@link Image}
+ * @param imageType 目标图片类型,{@link BufferedImage}中的常量,例如黑白等
+ * @param backgroundColor 背景色,{@code null} 表示默认背景色(黑色或者透明)
+ * @return {@link BufferedImage}
+ * @see BufferedImage#TYPE_INT_RGB
+ * @see BufferedImage#TYPE_INT_ARGB
+ * @see BufferedImage#TYPE_INT_ARGB_PRE
+ * @see BufferedImage#TYPE_INT_BGR
+ * @see BufferedImage#TYPE_3BYTE_BGR
+ * @see BufferedImage#TYPE_4BYTE_ABGR
+ * @see BufferedImage#TYPE_4BYTE_ABGR_PRE
+ * @see BufferedImage#TYPE_BYTE_GRAY
+ * @see BufferedImage#TYPE_USHORT_GRAY
+ * @see BufferedImage#TYPE_BYTE_BINARY
+ * @see BufferedImage#TYPE_BYTE_INDEXED
+ * @see BufferedImage#TYPE_USHORT_565_RGB
+ * @see BufferedImage#TYPE_USHORT_555_RGB
+ * @since 4.5.17
+ */
+ public static BufferedImage copyImage(Image img, int imageType, Color backgroundColor) {
+ // ensures that all the pixels loaded
+ // issue#1821@Github
+ img = new ImageIcon(img).getImage();
+
+ final BufferedImage bimage = new BufferedImage(
+ img.getWidth(null), img.getHeight(null), imageType);
+ final Graphics2D bGr = GraphicsUtil.createGraphics(bimage, backgroundColor);
+ try {
+ bGr.drawImage(img, 0, 0, null);
+ } finally {
+ bGr.dispose();
+ }
+
+ return bimage;
+ }
+
+ /**
+ * 创建与当前设备颜色模式兼容的 {@link BufferedImage}
+ *
+ * @param width 宽度
+ * @param height 高度
+ * @param transparency 透明模式,见 {@link Transparency}
+ * @return {@link BufferedImage}
+ * @since 5.7.13
+ */
+ public static BufferedImage createCompatibleImage(int width, int height, int transparency) throws HeadlessException {
+ GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
+ GraphicsDevice gs = ge.getDefaultScreenDevice();
+ GraphicsConfiguration gc = gs.getDefaultConfiguration();
+ return gc.createCompatibleImage(width, height, transparency);
+ }
+
+ /**
+ * 将Base64编码的图像信息转为 {@link BufferedImage}
+ *
+ * @param base64 图像的Base64表示
+ * @return {@link BufferedImage}
+ * @throws RuntimeException IO异常
+ */
+ public static BufferedImage toImage(String base64) throws RuntimeException {
+ return toImage(Base64Util.decodeBase64(base64));
+ }
+
+ /**
+ * 将的图像bytes转为 {@link BufferedImage}
+ *
+ * @param imageBytes 图像bytes
+ * @return {@link BufferedImage}
+ * @throws RuntimeException IO异常
+ */
+ public static BufferedImage toImage(byte[] imageBytes) throws RuntimeException {
+ return read(new ByteArrayInputStream(imageBytes));
+ }
+
+ /**
+ * 将图片对象转换为InputStream形式
+ *
+ * @param image 图片对象
+ * @param imageType 图片类型
+ * @return Base64的字符串表现形式
+ * @since 4.2.4
+ */
+ public static ByteArrayInputStream toStream(Image image, String imageType) {
+ return IoUtil.toStream(toBytes(image, imageType));
+ }
+
+ /**
+ * 将图片对象转换为Base64的Data URI形式,格式为:data:image/[imageType];base64,[data]
+ *
+ * @param image 图片对象
+ * @param imageType 图片类型
+ * @return Base64的字符串表现形式
+ * @since 5.3.6
+ */
+ public static String toBase64DataUri(Image image, String imageType) {
+ return getDataUri(
+ "image/" + imageType, null, "base64",
+ toBase64(image, imageType));
+ }
+
+ /**
+ * 将图片对象转换为Base64形式
+ *
+ * @param image 图片对象
+ * @param imageType 图片类型
+ * @return Base64的字符串表现形式
+ * @since 4.1.8
+ */
+ public static String toBase64(Image image, String imageType) {
+ return Base64Util.encodeBase64(toBytes(image, imageType));
+ }
+
+ /**
+ * 将图片对象转换为bytes形式
+ *
+ * @param image 图片对象
+ * @param imageType 图片类型
+ * @return Base64的字符串表现形式
+ * @since 5.2.4
+ */
+ public static byte[] toBytes(Image image, String imageType) {
+ final ByteArrayOutputStream out = new ByteArrayOutputStream();
+ write(image, imageType, out);
+ return out.toByteArray();
+ }
+
+ /**
+ * 根据文字创建PNG图片
+ *
+ * @param str 文字
+ * @param font 字体{@link Font}
+ * @param backgroundColor 背景颜色,默认透明
+ * @param fontColor 字体颜色,默认黑色
+ * @param out 图片输出地
+ * @throws RuntimeException IO异常
+ */
+ public static void createImage(String str, Font font, Color backgroundColor, Color fontColor, ImageOutputStream out) throws RuntimeException {
+ writePng(createImage(str, font, backgroundColor, fontColor, BufferedImage.TYPE_INT_ARGB), out);
+ }
+
+ /**
+ * 根据文字创建透明背景的PNG图片
+ *
+ * @param str 文字
+ * @param font 字体{@link Font}
+ * @param fontColor 字体颜色,默认黑色
+ * @param out 图片输出地
+ * @throws RuntimeException IO异常
+ */
+ public static void createTransparentImage(String str, Font font, Color fontColor, ImageOutputStream out) throws RuntimeException {
+ writePng(createImage(str, font, null, fontColor, BufferedImage.TYPE_INT_ARGB), out);
+ }
+
+ /**
+ * 根据文字创建图片
+ *
+ * @param str 文字
+ * @param font 字体{@link Font}
+ * @param backgroundColor 背景颜色,默认透明
+ * @param fontColor 字体颜色,默认黑色
+ * @param imageType 图片类型,见:{@link BufferedImage}
+ * @return 图片
+ * @throws RuntimeException IO异常
+ */
+ public static BufferedImage createImage(String str, Font font, Color backgroundColor, Color fontColor, int imageType) throws RuntimeException {
+ // 获取font的样式应用在str上的整个矩形
+ final Rectangle2D r = getRectangle(str, font);
+ // 获取单个字符的高度
+ int unitHeight = (int)Math.floor(r.getHeight());
+ // 获取整个str用了font样式的宽度这里用四舍五入后+1保证宽度绝对能容纳这个字符串作为图片的宽度
+ int width = (int)Math.round(r.getWidth()) + 1;
+ // 把单个字符的高度+3保证高度绝对能容纳字符串作为图片的高度
+ int height = unitHeight + 3;
+
+ // 创建图片
+ BufferedImage image = new BufferedImage(width, height, imageType);
+ Graphics g = image.getGraphics();
+ if (null != backgroundColor) {
+ // 先用背景色填充整张图片,也就是背景
+ g.setColor(backgroundColor);
+ g.fillRect(0, 0, width, height);
+ }
+
+ g.setColor(ObjectUtils.defaultIfNull(fontColor, Color.BLACK));
+ g.setFont(font);// 设置画笔字体
+ g.drawString(str, 0, font.getSize());// 画出字符串
+ g.dispose();
+
+ return image;
+ }
+
+ /**
+ * 获取font的样式应用在str上的整个矩形
+ *
+ * @param str 字符串,必须非空
+ * @param font 字体,必须非空
+ * @return {@link Rectangle2D}
+ * @since 5.3.3
+ */
+ public static Rectangle2D getRectangle(String str, Font font) {
+ return font.getStringBounds(str,
+ new FontRenderContext(AffineTransform.getScaleInstance(1, 1),
+ false,
+ false));
+ }
+
+ /**
+ * 根据文件创建字体
+ * 首先尝试创建{@link Font#TRUETYPE_FONT}字体,此类字体无效则创建{@link Font#TYPE1_FONT}
+ *
+ * @param fontFile 字体文件
+ * @return {@link Font}
+ * @since 3.0.9
+ */
+ public static Font createFont(File fontFile) {
+ return FontUtil.createFont(fontFile);
+ }
+
+ /**
+ * 根据文件创建字体
+ * 首先尝试创建{@link Font#TRUETYPE_FONT}字体,此类字体无效则创建{@link Font#TYPE1_FONT}
+ *
+ * @param fontStream 字体流
+ * @return {@link Font}
+ * @since 3.0.9
+ */
+ public static Font createFont(InputStream fontStream) {
+ return FontUtil.createFont(fontStream);
+ }
+
+ /**
+ * 创建{@link Graphics2D}
+ *
+ * @param image {@link BufferedImage}
+ * @param color {@link Color}背景颜色以及当前画笔颜色
+ * @return {@link Graphics2D}
+ * @see GraphicsUtil#createGraphics(BufferedImage, Color)
+ * @since 3.2.3
+ */
+ public static Graphics2D createGraphics(BufferedImage image, Color color) {
+ return GraphicsUtil.createGraphics(image, color);
+ }
+
+ /**
+ * 写出图像为JPG格式
+ *
+ * @param image {@link Image}
+ * @param destImageStream 写出到的目标流
+ * @throws RuntimeException IO异常
+ */
+ public static void writeJpg(Image image, ImageOutputStream destImageStream) throws RuntimeException {
+ write(image, IMAGE_TYPE_JPG, destImageStream);
+ }
+
+ /**
+ * 写出图像为PNG格式
+ *
+ * @param image {@link Image}
+ * @param destImageStream 写出到的目标流
+ * @throws RuntimeException IO异常
+ */
+ public static void writePng(Image image, ImageOutputStream destImageStream) throws RuntimeException {
+ write(image, IMAGE_TYPE_PNG, destImageStream);
+ }
+
+ /**
+ * 写出图像为JPG格式
+ *
+ * @param image {@link Image}
+ * @param out 写出到的目标流
+ * @throws RuntimeException IO异常
+ * @since 4.0.10
+ */
+ public static void writeJpg(Image image, OutputStream out) throws RuntimeException {
+ write(image, IMAGE_TYPE_JPG, out);
+ }
+
+ /**
+ * 写出图像为PNG格式
+ *
+ * @param image {@link Image}
+ * @param out 写出到的目标流
+ * @throws RuntimeException IO异常
+ * @since 4.0.10
+ */
+ public static void writePng(Image image, OutputStream out) throws RuntimeException {
+ write(image, IMAGE_TYPE_PNG, out);
+ }
+
+ /**
+ * 按照目标格式写出图像:GIF=》JPG、GIF=》PNG、PNG=》JPG、PNG=》GIF(X)、BMP=》PNG
+ * 此方法并不关闭流
+ *
+ * @param srcStream 源图像流
+ * @param formatName 包含格式非正式名称的 String:如JPG、JPEG、GIF等
+ * @param destStream 目标图像输出流
+ * @since 5.0.0
+ */
+ public static void write(ImageInputStream srcStream, String formatName, ImageOutputStream destStream) {
+ write(read(srcStream), formatName, destStream);
+ }
+
+ /**
+ * 写出图像:GIF=》JPG、GIF=》PNG、PNG=》JPG、PNG=》GIF(X)、BMP=》PNG
+ * 此方法并不关闭流
+ *
+ * @param image {@link Image}
+ * @param imageType 图片类型(图片扩展名)
+ * @param out 写出到的目标流
+ * @throws RuntimeException IO异常
+ * @since 3.1.2
+ */
+ public static void write(Image image, String imageType, OutputStream out) throws RuntimeException {
+ write(image, imageType, getImageOutputStream(out));
+ }
+
+ /**
+ * 写出图像为指定格式:GIF=》JPG、GIF=》PNG、PNG=》JPG、PNG=》GIF(X)、BMP=》PNG
+ * 此方法并不关闭流
+ *
+ * @param image {@link Image}
+ * @param imageType 图片类型(图片扩展名)
+ * @param destImageStream 写出到的目标流
+ * @return 是否成功写出,如果返回false表示未找到合适的Writer
+ * @throws RuntimeException IO异常
+ * @since 3.1.2
+ */
+ public static boolean write(Image image, String imageType, ImageOutputStream destImageStream) throws RuntimeException {
+ return write(image, imageType, destImageStream, 1);
+ }
+
+ /**
+ * 写出图像为指定格式
+ *
+ * @param image {@link Image}
+ * @param imageType 图片类型(图片扩展名)
+ * @param destImageStream 写出到的目标流
+ * @param quality 质量,数字为0~1(不包括0和1)表示质量压缩比,除此数字外设置表示不压缩
+ * @return 是否成功写出,如果返回false表示未找到合适的Writer
+ * @throws RuntimeException IO异常
+ * @since 4.3.2
+ */
+ public static boolean write(Image image, String imageType, ImageOutputStream destImageStream, float quality) throws RuntimeException {
+ return write(image, imageType, destImageStream, quality, null);
+ }
+
+ /**
+ * 写出图像为指定格式
+ *
+ * @param image {@link Image}
+ * @param imageType 图片类型(图片扩展名)
+ * @param destImageStream 写出到的目标流
+ * @param quality 质量,数字为0~1(不包括0和1)表示质量压缩比,除此数字外设置表示不压缩
+ * @param backgroundColor 背景色{@link Color}
+ * @return 是否成功写出,如果返回false表示未找到合适的Writer
+ * @throws RuntimeException IO异常
+ * @since 4.3.2
+ */
+ public static boolean write(Image image, String imageType, ImageOutputStream destImageStream, float quality, Color backgroundColor)
+ throws RuntimeException {
+ if (StringTools.isBlank(imageType)) {
+ imageType = IMAGE_TYPE_JPG;
+ }
+
+ final BufferedImage bufferedImage = toBufferedImage(image, imageType, backgroundColor);
+ final ImageWriter writer = getWriter(bufferedImage, imageType);
+ return write(bufferedImage, writer, destImageStream, quality);
+ }
+
+ /**
+ * 写出图像为目标文件扩展名对应的格式
+ *
+ * @param image {@link Image}
+ * @param targetFile 目标文件
+ * @throws RuntimeException IO异常
+ * @since 3.1.0
+ */
+ public static void write(Image image, File targetFile) throws RuntimeException {
+ FileTools.touch(targetFile);
+ ImageOutputStream out = null;
+ try {
+ out = getImageOutputStream(targetFile);
+ write(image, FileNameUtil.extName(targetFile), out);
+ } finally {
+ IoUtil.close(out);
+ }
+ }
+
+ /**
+ * 通过{@link ImageWriter}写出图片到输出流
+ *
+ * @param image 图片
+ * @param writer {@link ImageWriter}
+ * @param output 输出的Image流{@link ImageOutputStream}
+ * @param quality 质量,数字为0~1(不包括0和1)表示质量压缩比,除此数字外设置表示不压缩
+ * @return 是否成功写出
+ * @since 4.3.2
+ */
+ public static boolean write(Image image, ImageWriter writer, ImageOutputStream output, float quality) {
+ if (writer == null) {
+ return false;
+ }
+
+ writer.setOutput(output);
+ final RenderedImage renderedImage = castToRenderedImage(image, IMAGE_TYPE_JPG);
+ // 设置质量
+ ImageWriteParam imgWriteParams = null;
+ if (quality > 0 && quality < 1) {
+ imgWriteParams = writer.getDefaultWriteParam();
+ if (imgWriteParams.canWriteCompressed()) {
+ imgWriteParams.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
+ imgWriteParams.setCompressionQuality(quality);
+ final ColorModel colorModel = renderedImage.getColorModel();// ColorModel.getRGBdefault();
+ imgWriteParams.setDestinationType(new ImageTypeSpecifier(colorModel, colorModel.createCompatibleSampleModel(16, 16)));
+ }
+ }
+
+ try {
+ if (null != imgWriteParams) {
+ writer.write(null, new IIOImage(renderedImage, null, null), imgWriteParams);
+ } else {
+ writer.write(renderedImage);
+ }
+ output.flush();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ } finally {
+ writer.dispose();
+ }
+ return true;
+ }
+
+ /**
+ * 获得{@link ImageReader}
+ *
+ * @param type 图片文件类型,例如 "jpeg" 或 "tiff"
+ * @return {@link ImageReader}
+ */
+ public static ImageReader getReader(String type) {
+ final Iterator
+ * 1. 颜色的英文名(大小写皆可)
+ * 2. 16进制表示,例如:#fcf6d6或者$fcf6d6
+ * 3. RGB形式,例如:13,148,252
+ *
+ *
+ * 可以使用灰度 (gray)等
+ *
+ * @param colorSpace 颜色模式,如灰度等
+ * @param image 被转换的图片
+ * @return 转换后的图片
+ * @since 5.7.8
+ */
+ public static BufferedImage colorConvert(ColorSpace colorSpace, BufferedImage image) {
+ return filter(new ColorConvertOp(colorSpace, null), image);
+ }
+
+ /**
+ * 转换图片
+ * 可以使用一系列平移 (translation)、缩放 (scale)、翻转 (flip)、旋转 (rotation) 和错切 (shear) 来构造仿射变换。
+ *
+ * @param xform 2D仿射变换,它执行从 2D 坐标到其他 2D 坐标的线性映射,保留了线的“直线性”和“平行性”。
+ * @param image 被转换的图片
+ * @return 转换后的图片
+ * @since 5.7.8
+ */
+ public static BufferedImage transform(AffineTransform xform, BufferedImage image) {
+ return filter(new AffineTransformOp(xform, null), image);
+ }
+
+ /**
+ * 图片过滤转换
+ *
+ * @param op 过滤操作实现,如二维转换可传入{@link AffineTransformOp}
+ * @param image 原始图片
+ * @return 过滤后的图片
+ * @since 5.7.8
+ */
+ public static BufferedImage filter(BufferedImageOp op, BufferedImage image) {
+ return op.filter(image, null);
+ }
+
+ /**
+ * 图片滤镜,借助 {@link ImageFilter}实现,实现不同的图片滤镜
+ *
+ * @param filter 滤镜实现
+ * @param image 图片
+ * @return 滤镜后的图片
+ * @since 5.7.8
+ */
+ public static Image filter(ImageFilter filter, Image image) {
+ return Toolkit.getDefaultToolkit().createImage(
+ new FilteredImageSource(image.getSource(), filter));
+ }
+
+ /**
+ * 刷新和释放{@link Image} 资源
+ *
+ * @param image {@link Image}
+ */
+ public static void flush(Image image) {
+ if (null != image) {
+ image.flush();
+ }
+ }
+
+ /**
+ * Data URI Scheme封装。data URI scheme 允许我们使用内联(inline-code)的方式在网页中包含数据,
+ * 目的是将一些小的数据,直接嵌入到网页中,从而不用再从外部文件载入。常用于将图片嵌入网页。
+ *
+ *
+ * data:[<mime type>][;charset=<charset>][;<encoding>],<encoded data>
+ *
+ *
+ * @param mimeType 可选项(null表示无),数据类型(image/png、text/plain等)
+ * @param charset 可选项(null表示无),源文本的字符集编码方式
+ * @param encoding 数据编码方式(US-ASCII,BASE64等)
+ * @param data 编码后的数据
+ * @return Data URI字符串
+ * @since 5.3.6
+ */
+ public static String getDataUri(String mimeType, Charset charset, String encoding, String data) {
+ final StringBuilder builder = new StringBuilder("data:");
+ if (StringTools.isNotBlank(mimeType)) {
+ builder.append(mimeType);
+ }
+ if (null != charset) {
+ builder.append(";charset=").append(charset.name());
+ }
+ if (StringTools.isNotBlank(encoding)) {
+ builder.append(';').append(encoding);
+ }
+ builder.append(',').append(data);
+
+ return builder.toString();
+ }
+}
diff --git a/src/main/java/com/luna/common/img/LabColor.java b/src/main/java/com/luna/common/img/LabColor.java
new file mode 100644
index 000000000..d28c885ba
--- /dev/null
+++ b/src/main/java/com/luna/common/img/LabColor.java
@@ -0,0 +1,91 @@
+package com.luna.common.img;
+
+import com.luna.common.check.Assert;
+
+import java.awt.*;
+import java.awt.color.ColorSpace;
+
+/**
+ * 表示以 LAB 形式存储的颜色。
+ *
+ *
+ *
+ * @author Tom Xin
+ * @since 5.8.7
+ */
+public class LabColor {
+
+ private static final ColorSpace XYZ_COLOR_SPACE = ColorSpace.getInstance(ColorSpace.CS_CIEXYZ);
+
+ /**
+ * L: 亮度
+ */
+ private final double l;
+ /**
+ * A: 正数代表红色,负端代表绿色
+ */
+ private final double a;
+ /**
+ * B: 正数代表黄色,负端代表蓝色
+ */
+ private final double b;
+
+ public LabColor(Integer rgb) {
+ this((rgb != null) ? new Color(rgb) : null);
+ }
+
+ public LabColor(Color color) {
+ Assert.notNull(color, "Color must not be null");
+ final float[] lab = fromXyz(color.getColorComponents(XYZ_COLOR_SPACE, null));
+ this.l = lab[0];
+ this.a = lab[1];
+ this.b = lab[2];
+ }
+
+ /**
+ * 获取颜色差
+ *
+ * @param other 其他Lab颜色
+ * @return 颜色差
+ */
+ // See https://en.wikipedia.org/wiki/Color_difference#CIE94
+ public double getDistance(LabColor other) {
+ double c1 = Math.sqrt(this.a * this.a + this.b * this.b);
+ double deltaC = c1 - Math.sqrt(other.a * other.a + other.b * other.b);
+ double deltaA = this.a - other.a;
+ double deltaB = this.b - other.b;
+ double deltaH = Math.sqrt(Math.max(0.0, deltaA * deltaA + deltaB * deltaB - deltaC * deltaC));
+ return Math.sqrt(Math.max(0.0, Math.pow((this.l - other.l), 2)
+ + Math.pow(deltaC / (1 + 0.045 * c1), 2) + Math.pow(deltaH / (1 + 0.015 * c1), 2.0)));
+ }
+
+ private float[] fromXyz(float[] xyz) {
+ return fromXyz(xyz[0], xyz[1], xyz[2]);
+ }
+
+ /**
+ * 从xyz换算
+ * L=116f(y)-16
+ * a=500[f(x/0.982)-f(y)]
+ * b=200[f(y)-f(z/1.183 )]
+ * 其中: f(x)=7.787x+0.138, x<0.008856; f(x)=(x)1/3,x>0.008856
+ *
+ * @param x X
+ * @param y Y
+ * @param z Z
+ * @return Lab
+ */
+ private static float[] fromXyz(float x, float y, float z) {
+ final double l = (f(y) - 16.0) * 116.0;
+ final double a = (f(x) - f(y)) * 500.0;
+ final double b = (f(y) - f(z)) * 200.0;
+ return new float[] {(float)l, (float)a, (float)b};
+ }
+
+ private static double f(double t) {
+ return (t > (216.0 / 24389.0)) ? Math.cbrt(t) : (1.0 / 3.0) * Math.pow(29.0 / 6.0, 2) * t + (4.0 / 29.0);
+ }
+}
diff --git a/src/main/java/com/luna/common/img/ScaleType.java b/src/main/java/com/luna/common/img/ScaleType.java
new file mode 100755
index 000000000..3466994b4
--- /dev/null
+++ b/src/main/java/com/luna/common/img/ScaleType.java
@@ -0,0 +1,43 @@
+package com.luna.common.img;
+
+import java.awt.*;
+
+/**
+ * 图片缩略算法类型
+ *
+ * @author looly
+ * @since 4.5.8
+ */
+public enum ScaleType {
+
+ /** 默认 */
+ DEFAULT(Image.SCALE_DEFAULT),
+ /** 快速 */
+ FAST(Image.SCALE_FAST),
+ /** 平滑 */
+ SMOOTH(Image.SCALE_SMOOTH),
+ /** 使用 ReplicateScaleFilter 类中包含的图像缩放算法 */
+ REPLICATE(Image.SCALE_REPLICATE),
+ /** Area Averaging算法 */
+ AREA_AVERAGING(Image.SCALE_AREA_AVERAGING);
+
+ /**
+ * 构造
+ *
+ * @param value 缩放方式
+ * @see Image#SCALE_DEFAULT
+ * @see Image#SCALE_FAST
+ * @see Image#SCALE_SMOOTH
+ * @see Image#SCALE_REPLICATE
+ * @see Image#SCALE_AREA_AVERAGING
+ */
+ ScaleType(int value) {
+ this.value = value;
+ }
+
+ private final int value;
+
+ public int getValue() {
+ return this.value;
+ }
+}
diff --git a/src/main/java/com/luna/common/img/gif/AnimatedGifEncoder.java b/src/main/java/com/luna/common/img/gif/AnimatedGifEncoder.java
new file mode 100755
index 000000000..48aa444a7
--- /dev/null
+++ b/src/main/java/com/luna/common/img/gif/AnimatedGifEncoder.java
@@ -0,0 +1,585 @@
+package com.luna.common.img.gif;
+
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.awt.image.DataBufferByte;
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+
+/**
+ * 动态GIF动画生成器,可生成一个或多个帧的GIF。
+ *
+ *
+ * Example:
+ * AnimatedGifEncoder e = new AnimatedGifEncoder();
+ * e.start(outputFileName);
+ * e.setDelay(1000); // 1 frame per sec
+ * e.addFrame(image1);
+ * e.addFrame(image2);
+ * e.finish();
+ *
+ *
+ * {@code
+ * GifDecoder d = new GifDecoder();
+ * d.read("sample.gif");
+ * int n = d.getFrameCount();
+ * for (int i = 0; i < n; i++) {
+ * BufferedImage frame = d.getFrame(i); // frame i
+ * int t = d.getDelay(i); // display duration of frame in milliseconds
+ * // do something with frame
+ * }
+ * }
+ *
+ *
+ * 用户通过实现此接口,实现监听剪贴板内容变化
+ *
+ * @author looly
+ * @since 4.5.6
+ */
+public interface ClipboardListener {
+ /**
+ * 剪贴板变动触发的事件方法
+ * 在此事件中对剪贴板设置值无效,如若修改,需返回修改内容
+ *
+ * @param clipboard 剪贴板对象
+ * @param contents 内容
+ * @return 如果对剪贴板内容做修改,则返回修改的内容,{@code null}表示保留原内容
+ */
+ Transferable onChange(Clipboard clipboard, Transferable contents);
+}
diff --git a/src/main/java/com/luna/common/swing/ClipboardMonitor.java b/src/main/java/com/luna/common/swing/ClipboardMonitor.java
new file mode 100755
index 000000000..8a74b5246
--- /dev/null
+++ b/src/main/java/com/luna/common/swing/ClipboardMonitor.java
@@ -0,0 +1,217 @@
+package com.luna.common.swing;
+
+import com.luna.common.utils.ObjectUtils;
+
+import java.awt.datatransfer.Clipboard;
+import java.awt.datatransfer.ClipboardOwner;
+import java.awt.datatransfer.Transferable;
+import java.io.Closeable;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+/**
+ * 剪贴板监听
+ *
+ * @author looly
+ * @since 4.5.6
+ */
+public enum ClipboardMonitor implements ClipboardOwner, Runnable, Closeable {
+ INSTANCE;
+
+ /** 默认重试此时:10 */
+ public static final int DEFAULT_TRY_COUNT = 10;
+ /** 默认重试等待:100 */
+ public static final long DEFAULT_DELAY = 100;
+
+ /** 重试次数 */
+ private int tryCount;
+ /** 重试等待 */
+ private long delay;
+ /** 系统剪贴板对象 */
+ private final Clipboard clipboard;
+ /** 监听事件处理 */
+ private final Set
+ * Desktop 类允许 Java 应用程序启动已在本机桌面上注册的关联应用程序,以处理 URI 或文件。
+ *
+ * @author looly
+ * @since 4.5.7
+ */
+public class DesktopUtil {
+
+ /**
+ * 获得{@link Desktop}
+ *
+ * @return {@link Desktop}
+ */
+ public static Desktop getDsktop() {
+ return Desktop.getDesktop();
+ }
+
+ /**
+ * 使用平台默认浏览器打开指定URL地址
+ *
+ * @param url URL地址
+ */
+ public static void browse(String url) {
+ try {
+ browse(new URI(StringTools.trim(url)));
+ } catch (URISyntaxException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * 使用平台默认浏览器打开指定URI地址
+ *
+ * @param uri URI地址
+ * @since 4.6.3
+ */
+ public static void browse(URI uri) {
+ final Desktop dsktop = getDsktop();
+ try {
+ dsktop.browse(uri);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * 启动关联应用程序来打开文件
+ *
+ * @param file URL地址
+ */
+ public static void open(File file) {
+ final Desktop dsktop = getDsktop();
+ try {
+ dsktop.open(file);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * 启动关联编辑器应用程序并打开用于编辑的文件
+ *
+ * @param file 文件
+ */
+ public static void edit(File file) {
+ final Desktop dsktop = getDsktop();
+ try {
+ dsktop.edit(file);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * 使用关联应用程序的打印命令, 用本机桌面打印设备来打印文件
+ *
+ * @param file 文件
+ */
+ public static void print(File file) {
+ final Desktop dsktop = getDsktop();
+ try {
+ dsktop.print(file);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * 使用平台默认浏览器打开指定URL地址
+ *
+ * @param mailAddress 邮件地址
+ */
+ public static void mail(String mailAddress) {
+ final Desktop dsktop = getDsktop();
+ try {
+ URI uri = new URI(StringTools.trim(mailAddress));
+ dsktop.mail(uri);
+ } catch (IOException | URISyntaxException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/src/main/java/com/luna/common/swing/ImageSelection.java b/src/main/java/com/luna/common/swing/ImageSelection.java
new file mode 100755
index 000000000..909efb385
--- /dev/null
+++ b/src/main/java/com/luna/common/swing/ImageSelection.java
@@ -0,0 +1,64 @@
+package com.luna.common.swing;
+
+import java.awt.*;
+import java.awt.datatransfer.DataFlavor;
+import java.awt.datatransfer.Transferable;
+import java.awt.datatransfer.UnsupportedFlavorException;
+import java.io.Serializable;
+
+/**
+ * 图片转换器,用于将图片对象转换为剪贴板支持的对象
+ * 此对象也用于将图像文件和{@link DataFlavor#imageFlavor} 元信息对应
+ *
+ * @author looly
+ * @since 4.5.6
+ */
+public class ImageSelection implements Transferable, Serializable {
+ private static final long serialVersionUID = 1L;
+
+ private final Image image;
+
+ /**
+ * 构造
+ *
+ * @param image 图片
+ */
+ public ImageSelection(Image image) {
+ this.image = image;
+ }
+
+ /**
+ * 获取元数据类型信息
+ *
+ * @return 元数据类型列表
+ */
+ @Override
+ public DataFlavor[] getTransferDataFlavors() {
+ return new DataFlavor[] {DataFlavor.imageFlavor};
+ }
+
+ /**
+ * 是否支持指定元数据类型
+ *
+ * @param flavor 元数据类型
+ * @return 是否支持
+ */
+ @Override
+ public boolean isDataFlavorSupported(DataFlavor flavor) {
+ return DataFlavor.imageFlavor.equals(flavor);
+ }
+
+ /**
+ * 获取图片
+ *
+ * @param flavor 元数据类型
+ * @return 转换后的对象
+ */
+ @Override
+ public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException {
+ if (false == DataFlavor.imageFlavor.equals(flavor)) {
+ throw new UnsupportedFlavorException(flavor);
+ }
+ return image;
+ }
+}
diff --git a/src/main/java/com/luna/common/swing/RobotUtil.java b/src/main/java/com/luna/common/swing/RobotUtil.java
new file mode 100755
index 000000000..3c38caf41
--- /dev/null
+++ b/src/main/java/com/luna/common/swing/RobotUtil.java
@@ -0,0 +1,230 @@
+package com.luna.common.swing;
+
+import java.awt.*;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+import java.awt.image.BufferedImage;
+import java.io.File;
+
+import com.luna.common.exception.UtilException;
+import com.luna.common.img.ImgUtil;
+
+/**
+ * {@link Robot} 封装工具类,提供截屏等工具
+ *
+ * @author looly
+ * @since 4.1.14
+ */
+public class RobotUtil {
+
+ public static void main(String[] args) {
+ RobotUtil robotUtil = new RobotUtil();
+ robotUtil.delay = 1000;
+ click();
+ robotUtil.keyClick(KeyEvent.VK_S, KeyEvent.VK_LEFT);
+ }
+
+ private static final Robot ROBOT;
+ private static int delay;
+
+ static {
+ try {
+ ROBOT = new Robot();
+ } catch (AWTException e) {
+ throw new UtilException(e);
+ }
+ }
+
+ /**
+ * 获取 Robot 单例实例
+ *
+ * @return {@link Robot}单例对象
+ * @since 5.7.6
+ */
+ public static Robot getRobot() {
+ return ROBOT;
+ }
+
+ /**
+ * 设置默认的延迟时间
+ * 当按键执行完后的等待时间,也可以用ThreadUtil.sleep方法代替
+ *
+ * @param delayMillis 等待毫秒数
+ * @since 4.5.7
+ */
+ public static void setDelay(int delayMillis) {
+ delay = delayMillis;
+ }
+
+ /**
+ * 获取全局默认的延迟时间
+ *
+ * @return 全局默认的延迟时间
+ * @since 5.7.6
+ */
+ public static int getDelay() {
+ return delay;
+ }
+
+ /**
+ * 模拟鼠标移动
+ *
+ * @param x 移动到的x坐标
+ * @param y 移动到的y坐标
+ * @since 4.5.7
+ */
+ public static void mouseMove(int x, int y) {
+ ROBOT.mouseMove(x, y);
+ }
+
+ /**
+ * 模拟单击
+ * 鼠标单击包括鼠标左键的按下和释放
+ *
+ * @since 4.5.7
+ */
+ public static void click() {
+ ROBOT.mousePress(InputEvent.BUTTON1_MASK);
+ ROBOT.mouseRelease(InputEvent.BUTTON1_MASK);
+ delay();
+ }
+
+ /**
+ * 模拟右键单击
+ * 鼠标单击包括鼠标右键的按下和释放
+ *
+ * @since 4.5.7
+ */
+ public static void rightClick() {
+ ROBOT.mousePress(InputEvent.BUTTON3_MASK);
+ ROBOT.mouseRelease(InputEvent.BUTTON3_MASK);
+ delay();
+ }
+
+ /**
+ * 模拟鼠标滚轮滚动
+ *
+ * @param wheelAmt 滚动数,负数表示向前滚动,正数向后滚动
+ * @since 4.5.7
+ */
+ public static void mouseWheel(int wheelAmt) {
+ ROBOT.mouseWheel(wheelAmt);
+ delay();
+ }
+
+ /**
+ * 模拟键盘点击
+ * 包括键盘的按下和释放
+ *
+ * @param keyCodes 按键码列表,见{@link KeyEvent}
+ * @since 4.5.7
+ */
+ public static void keyClick(int... keyCodes) {
+ for (int keyCode : keyCodes) {
+ ROBOT.keyPress(keyCode);
+ ROBOT.keyRelease(keyCode);
+ }
+ delay();
+ }
+
+ /**
+ * 打印输出指定字符串(借助剪贴板)
+ *
+ * @param str 字符串
+ */
+ public static void keyPressString(String str) {
+ ClipboardUtil.setStr(str);
+ keyPressWithCtrl(KeyEvent.VK_V);// 粘贴
+ delay();
+ }
+
+ /**
+ * shift+ 按键
+ *
+ * @param key 按键
+ */
+ public static void keyPressWithShift(int key) {
+ ROBOT.keyPress(KeyEvent.VK_SHIFT);
+ ROBOT.keyPress(key);
+ ROBOT.keyRelease(key);
+ ROBOT.keyRelease(KeyEvent.VK_SHIFT);
+ delay();
+ }
+
+ /**
+ * ctrl+ 按键
+ *
+ * @param key 按键
+ */
+ public static void keyPressWithCtrl(int key) {
+ ROBOT.keyPress(KeyEvent.VK_CONTROL);
+ ROBOT.keyPress(key);
+ ROBOT.keyRelease(key);
+ ROBOT.keyRelease(KeyEvent.VK_CONTROL);
+ delay();
+ }
+
+ /**
+ * alt+ 按键
+ *
+ * @param key 按键
+ */
+ public static void keyPressWithAlt(int key) {
+ ROBOT.keyPress(KeyEvent.VK_ALT);
+ ROBOT.keyPress(key);
+ ROBOT.keyRelease(key);
+ ROBOT.keyRelease(KeyEvent.VK_ALT);
+ delay();
+ }
+
+ /**
+ * 截取全屏
+ *
+ * @return 截屏的图片
+ */
+ public static BufferedImage captureScreen() {
+ return captureScreen(ScreenUtil.getRectangle());
+ }
+
+ /**
+ * 截取全屏到文件
+ *
+ * @param outFile 写出到的文件
+ * @return 写出到的文件
+ */
+ public static File captureScreen(File outFile) {
+ ImgUtil.write(captureScreen(), outFile);
+ return outFile;
+ }
+
+ /**
+ * 截屏
+ *
+ * @param screenRect 截屏的矩形区域
+ * @return 截屏的图片
+ */
+ public static BufferedImage captureScreen(Rectangle screenRect) {
+ return ROBOT.createScreenCapture(screenRect);
+ }
+
+ /**
+ * 截屏
+ *
+ * @param screenRect 截屏的矩形区域
+ * @param outFile 写出到的文件
+ * @return 写出到的文件
+ */
+ public static File captureScreen(Rectangle screenRect, File outFile) {
+ ImgUtil.write(captureScreen(screenRect), outFile);
+ return outFile;
+ }
+
+ /**
+ * 等待指定毫秒数
+ */
+ public static void delay() {
+ if (delay > 0) {
+ ROBOT.delay(delay);
+ }
+ }
+}
diff --git a/src/main/java/com/luna/common/swing/ScreenUtil.java b/src/main/java/com/luna/common/swing/ScreenUtil.java
new file mode 100755
index 000000000..fd62f0cc3
--- /dev/null
+++ b/src/main/java/com/luna/common/swing/ScreenUtil.java
@@ -0,0 +1,87 @@
+package com.luna.common.swing;
+
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.io.File;
+
+/**
+ * 屏幕相关(当前显示设置)工具类
+ *
+ * @author looly
+ * @since 4.1.14
+ */
+public class ScreenUtil {
+ public static Dimension dimension = Toolkit.getDefaultToolkit().getScreenSize();
+
+ /**
+ * 获取屏幕宽度
+ *
+ * @return 屏幕宽度
+ */
+ public static int getWidth() {
+ return (int)dimension.getWidth();
+ }
+
+ /**
+ * 获取屏幕高度
+ *
+ * @return 屏幕高度
+ */
+ public static int getHeight() {
+ return (int)dimension.getHeight();
+ }
+
+ /**
+ * 获取屏幕的矩形
+ *
+ * @return 屏幕的矩形
+ */
+ public static Rectangle getRectangle() {
+ return new Rectangle(getWidth(), getHeight());
+ }
+
+ // -------------------------------------------------------------------------------------------- 截屏
+ /**
+ * 截取全屏
+ *
+ * @return 截屏的图片
+ * @see RobotUtil#captureScreen()
+ */
+ public static BufferedImage captureScreen() {
+ return RobotUtil.captureScreen();
+ }
+
+ /**
+ * 截取全屏到文件
+ *
+ * @param outFile 写出到的文件
+ * @return 写出到的文件
+ * @see RobotUtil#captureScreen(File)
+ */
+ public static File captureScreen(File outFile) {
+ return RobotUtil.captureScreen(outFile);
+ }
+
+ /**
+ * 截屏
+ *
+ * @param screenRect 截屏的矩形区域
+ * @return 截屏的图片
+ * @see RobotUtil#captureScreen(Rectangle)
+ */
+ public static BufferedImage captureScreen(Rectangle screenRect) {
+ return RobotUtil.captureScreen(screenRect);
+ }
+
+ /**
+ * 截屏
+ *
+ * @param screenRect 截屏的矩形区域
+ * @param outFile 写出到的文件
+ * @return 写出到的文件
+ * @see RobotUtil#captureScreen(Rectangle, File)
+ */
+ public static File captureScreen(Rectangle screenRect, File outFile) {
+ return RobotUtil.captureScreen(screenRect, outFile);
+ }
+}
diff --git a/src/main/java/com/luna/common/swing/StrClipboardListener.java b/src/main/java/com/luna/common/swing/StrClipboardListener.java
new file mode 100755
index 000000000..49a092f0c
--- /dev/null
+++ b/src/main/java/com/luna/common/swing/StrClipboardListener.java
@@ -0,0 +1,34 @@
+package com.luna.common.swing;
+
+import java.awt.datatransfer.Clipboard;
+import java.awt.datatransfer.DataFlavor;
+import java.awt.datatransfer.Transferable;
+import java.io.Serializable;
+
+/**
+ * 剪贴板字符串内容监听
+ *
+ * @author looly
+ * @since 4.5.7
+ */
+public abstract class StrClipboardListener implements ClipboardListener, Serializable {
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public Transferable onChange(Clipboard clipboard, Transferable contents) {
+ if (contents.isDataFlavorSupported(DataFlavor.stringFlavor)) {
+ return onChange(clipboard, ClipboardUtil.getStr(contents));
+ }
+ return null;
+ }
+
+ /**
+ * 剪贴板变动触发的事件方法
+ * 在此事件中对剪贴板设置值无效,如若修改,需返回修改内容
+ *
+ * @param clipboard 剪贴板对象
+ * @param contents 内容
+ * @return 如果对剪贴板内容做修改,则返回修改的内容,{@code null}表示保留原内容
+ */
+ public abstract Transferable onChange(Clipboard clipboard, String contents);
+}
diff --git a/src/main/java/com/luna/common/swing/package-info.java b/src/main/java/com/luna/common/swing/package-info.java
new file mode 100755
index 000000000..51c781813
--- /dev/null
+++ b/src/main/java/com/luna/common/swing/package-info.java
@@ -0,0 +1,7 @@
+/**
+ * Swing和awt相关封装
+ *
+ * @author looly
+ *
+ */
+package com.luna.common.swing;
\ No newline at end of file
diff --git a/src/main/java/com/luna/common/utils/ObjectUtils.java b/src/main/java/com/luna/common/utils/ObjectUtils.java
index 1759ea26c..8497fdf2b 100755
--- a/src/main/java/com/luna/common/utils/ObjectUtils.java
+++ b/src/main/java/com/luna/common/utils/ObjectUtils.java
@@ -26,6 +26,8 @@
import com.luna.common.check.Assert;
import com.luna.common.text.StringTools;
+import static com.luna.common.regex.Validator.isNotEmpty;
+
/**
* Miscellaneous object utility methods.
*
@@ -94,6 +96,47 @@ public static boolean isCompatibleWithThrowsClause(Throwable ex, @Nullable Class
return false;
}
+ /**
+ * 是否包含{@code null}元素
+ *
+ * @param
+ * ObjectUtil.defaultIfNull(null, null) = null
+ * ObjectUtil.defaultIfNull(null, "") = ""
+ * ObjectUtil.defaultIfNull(null, "zz") = "zz"
+ * ObjectUtil.defaultIfNull("abc", *) = "abc"
+ * ObjectUtil.defaultIfNull(Boolean.TRUE, *) = Boolean.TRUE
+ *
+ *
+ * @param
@@ -18,6 +19,12 @@
*/
public class DesktopUtil {
+ public static List
* 当按键执行完后的等待时间,也可以用ThreadUtil.sleep方法代替
@@ -56,16 +64,6 @@ public static void setDelay(int delayMillis) {
delay = delayMillis;
}
- /**
- * 获取全局默认的延迟时间
- *
- * @return 全局默认的延迟时间
- * @since 5.7.6
- */
- public static int getDelay() {
- return delay;
- }
-
/**
* 模拟鼠标移动
*
@@ -207,6 +205,25 @@ public static BufferedImage captureScreen(Rectangle screenRect) {
return ROBOT.createScreenCapture(screenRect);
}
+ public static Robot getRobot(GraphicsDevice graphicsDevice) {
+ try {
+ return new Robot(graphicsDevice);
+ } catch (AWTException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * 对指定设备截屏
+ *
+ * @param robot
+ * @param screenRect
+ * @return
+ */
+ public static BufferedImage captureScreen(Robot robot, Rectangle screenRect) {
+ return robot.createScreenCapture(screenRect);
+ }
+
/**
* 截屏
*
@@ -219,6 +236,25 @@ public static File captureScreen(Rectangle screenRect, File outFile) {
return outFile;
}
+ /**
+ * 截屏
+ *
+ * 截屏的矩形区域
+ *
+ * @param x1
+ * @param y1
+ * @param x2
+ * @param y2
+ * @param outFile 写出到的文件
+ * @return 写出到的文件
+ *
+ */
+ public static File captureScreen(int x1, int y1, int x2, int y2, File outFile) {
+ Rectangle rectangle = new Rectangle(x1, y1, x2 - x1, y2 - y1);
+ ImgUtil.write(captureScreen(rectangle), outFile);
+ return outFile;
+ }
+
/**
* 等待指定毫秒数
*/
diff --git a/src/main/java/com/luna/common/swing/ScreenUtil.java b/src/main/java/com/luna/common/swing/ScreenUtil.java
index fd62f0cc3..bfead742f 100755
--- a/src/main/java/com/luna/common/swing/ScreenUtil.java
+++ b/src/main/java/com/luna/common/swing/ScreenUtil.java
@@ -73,6 +73,7 @@ public static BufferedImage captureScreen(Rectangle screenRect) {
return RobotUtil.captureScreen(screenRect);
}
+
/**
* 截屏
*
From 3cc9698fc1caa25dcc08656eb268908d6d61771e Mon Sep 17 00:00:00 2001
From: chenzhangyue <15696756582@163.com>
Date: Mon, 24 Jun 2024 14:00:08 +0800
Subject: [PATCH 4/4] :bookmark: fix
---
.../java/com/luna/common/swing/RobotUtil.java | 31 ++++++++++++++-----
1 file changed, 24 insertions(+), 7 deletions(-)
diff --git a/src/main/java/com/luna/common/swing/RobotUtil.java b/src/main/java/com/luna/common/swing/RobotUtil.java
index 339b91674..a4678e220 100755
--- a/src/main/java/com/luna/common/swing/RobotUtil.java
+++ b/src/main/java/com/luna/common/swing/RobotUtil.java
@@ -6,8 +6,10 @@
import java.awt.image.BufferedImage;
import java.io.File;
-import com.luna.common.exception.UtilException;
import com.luna.common.file.FileTools;
+import org.apache.commons.lang3.ArrayUtils;
+
+import com.luna.common.exception.UtilException;
import com.luna.common.img.ImgUtil;
/**
@@ -18,8 +20,8 @@
*/
public class RobotUtil {
- private static final Robot ROBOT;
- private static int delay;
+ private static Robot ROBOT;
+ private static int delay;
static {
try {
@@ -132,7 +134,8 @@ public static void keyClick(int... keyCodes) {
*/
public static void keyPressString(String str) {
ClipboardUtil.setStr(str);
- keyPressWithCtrl(KeyEvent.VK_V);// 粘贴
+ keyPressWithCtrl(KeyEvent.VK_V);
+ // 粘贴
delay();
}
@@ -215,7 +218,7 @@ public static Robot getRobot(GraphicsDevice graphicsDevice) {
/**
* 对指定设备截屏
- *
+ *
* @param robot
* @param screenRect
* @return
@@ -240,14 +243,14 @@ public static File captureScreen(Rectangle screenRect, File outFile) {
* 截屏
*
* 截屏的矩形区域
- *
+ *
* @param x1
* @param y1
* @param x2
* @param y2
* @param outFile 写出到的文件
* @return 写出到的文件
- *
+ *
*/
public static File captureScreen(int x1, int y1, int x2, int y2, File outFile) {
Rectangle rectangle = new Rectangle(x1, y1, x2 - x1, y2 - y1);
@@ -263,4 +266,18 @@ public static void delay() {
ROBOT.delay(delay);
}
}
+
+ public static Robot getRobot(Integer i) {
+ GraphicsDevice screenDevice = getScreenDevice(i);
+ return getRobot(screenDevice);
+ }
+
+ public static GraphicsDevice getScreenDevice(Integer i) {
+ GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
+ GraphicsDevice[] screenDevices = ge.getScreenDevices();
+ if (ArrayUtils.isEmpty(screenDevices)) {
+ throw new RuntimeException("没有找到可用的屏幕设备");
+ }
+ return screenDevices[i];
+ }
}