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 @@ io.github.lunasaw luna-common luna-common - 2.6.1 + 2.6.2 common is project which contains common utils https://github.com/lunasaw/luna-common diff --git a/src/main/java/com/luna/common/file/FileTools.java b/src/main/java/com/luna/common/file/FileTools.java index 4bf70f692..004a782c2 100644 --- a/src/main/java/com/luna/common/file/FileTools.java +++ b/src/main/java/com/luna/common/file/FileTools.java @@ -1,6 +1,7 @@ package com.luna.common.file; import static com.luna.common.file.PathUtil.del; +import static com.luna.common.os.OSinfo.isWindows; import static java.nio.file.Files.isDirectory; import java.io.*; @@ -14,13 +15,16 @@ import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; +import com.google.common.base.Splitter; import com.google.common.collect.Lists; import com.luna.common.check.Assert; +import com.luna.common.constant.CharPoolConstant; import com.luna.common.constant.Constant; import com.luna.common.exception.BaseException; import com.luna.common.file.visitor.MoveVisitor; import com.luna.common.io.IoUtil; import com.luna.common.text.CharsetUtil; +import com.luna.common.text.StringTools; /** * @author Luna @@ -584,6 +588,21 @@ public static BufferedOutputStream getOutputStream(File file) { return IoUtil.toBuffered(out); } + /** + * 创建文件及其父目录,如果这个文件存在,直接返回这个文件
+ * 此方法不对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 pathParts = Splitter.on('/').splitToList(fileName); + final int lastPartIndex = pathParts.size() - 1;// 目录个数 + for (int i = 0; i < lastPartIndex; i++) { + // 由于路径拆分,slip不检查,在最后一步检查 + outFile = new File(outFile, pathParts.get(i)); + } + // noinspection ResultOfMethodCallIgnored + outFile.mkdirs(); + // 最后一个部分如果非空,作为文件名 + fileName = pathParts.get(lastPartIndex); + } + return new File(outFile, fileName); + } + + /** + * 安全地级联创建目录 (确保并发环境下能创建成功) + * + *

+     *     并发环境下,假设 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 optionList = new ArrayList<>(2); + if (isOverride) { + optionList.add(StandardCopyOption.REPLACE_EXISTING); + } + if (isCopyAttributes) { + optionList.add(StandardCopyOption.COPY_ATTRIBUTES); + } + + if (src.isDirectory()) { + throw new RuntimeException(); + } + Files.copy(src.toPath(), dest.toPath(), optionList.toArray(new CopyOption[0])); + } } diff --git a/src/main/java/com/luna/common/img/BackgroundRemoval.java b/src/main/java/com/luna/common/img/BackgroundRemoval.java new file mode 100755 index 000000000..1f0dedc1d --- /dev/null +++ b/src/main/java/com/luna/common/img/BackgroundRemoval.java @@ -0,0 +1,361 @@ +package com.luna.common.img; + +import com.luna.common.file.FileNameUtil; +import com.luna.common.text.StringTools; +import org.apache.commons.lang3.ArrayUtils; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.imageio.ImageIO; +import javax.swing.*; + +/** + *

+ * 图片背景识别处理、背景替换、背景设置为矢量图 + *

+ *

+ * 根据一定规则算出图片背景色的RGB值,进行替换 + *

+ *

+ * 2020-05-21 16:36 + *

+ * + * @author Dai Yuanchuan + **/ +public class BackgroundRemoval { + + /** + * 目前暂时支持的图片类型数组 + * 其他格式的不保证结果 + */ + public static String[] IMAGES_TYPE = {"jpg", "png"}; + + /** + * 背景移除 + * 图片去底工具 + * 将 "纯色背景的图片" 还原成 "透明背景的图片" + * 将纯色背景的图片转成矢量图 + * 取图片边缘的像素点和获取到的图片主题色作为要替换的背景色 + * 再加入一定的容差值,然后将所有像素点与该颜色进行比较 + * 发现相同则将颜色不透明度设置为0,使颜色完全透明. + * + * @param inputPath 要处理图片的路径 + * @param outputPath 输出图片的路径 + * @param tolerance 容差值[根据图片的主题色,加入容差值,值的范围在0~255之间] + * @return 返回处理结果 true:图片处理完成 false:图片处理失败 + */ + public static boolean backgroundRemoval(String inputPath, String outputPath, int tolerance) { + return backgroundRemoval(new File(inputPath), new File(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(input, output, null, 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) { + if (fileTypeValidation(input, IMAGES_TYPE)) { + return false; + } + try { + // 获取图片左上、中上、右上、右中、右下、下中、左下、左中、8个像素点rgb的16进制值 + BufferedImage bufferedImage = ImageIO.read(input); + // 图片输出的格式为 png + return ImageIO.write(backgroundRemoval(bufferedImage, override, tolerance), "png", output); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } + + /** + * 背景移除 + * 图片去底工具 + * 将 "纯色背景的图片" 还原成 "透明背景的图片" + * 将纯色背景的图片转成矢量图 + * 取图片边缘的像素点和获取到的图片主题色作为要替换的背景色 + * 再加入一定的容差值,然后将所有像素点与该颜色进行比较 + * 发现相同则将颜色不透明度设置为0,使颜色完全透明. + * + * @param bufferedImage 需要进行处理的图片流 + * @param override 指定替换成的背景颜色 为null时背景为透明 + * @param tolerance 容差值[根据图片的主题色,加入容差值,值的取值范围在0~255之间] + * @return 返回处理好的图片流 + */ + public static BufferedImage backgroundRemoval(BufferedImage bufferedImage, Color override, int tolerance) { + // 容差值 最大255 最小0 + tolerance = Math.min(255, Math.max(tolerance, 0)); + // 绘制icon + ImageIcon imageIcon = new ImageIcon(bufferedImage); + BufferedImage image = new BufferedImage(imageIcon.getIconWidth(), imageIcon.getIconHeight(), + BufferedImage.TYPE_4BYTE_ABGR); + // 绘图工具 + Graphics graphics = image.getGraphics(); + graphics.drawImage(imageIcon.getImage(), 0, 0, imageIcon.getImageObserver()); + // 需要删除的RGB元素 + String[] removeRgb = getRemoveRgb(bufferedImage); + // 获取图片的大概主色调 + String mainColor = getMainColor(bufferedImage); + int alpha = 0; + for (int y = image.getMinY(); y < image.getHeight(); y++) { + for (int x = image.getMinX(); x < image.getWidth(); x++) { + // 获取像素的16进制 + int rgb = image.getRGB(x, y); + String hex = ImgUtil.toHex((rgb & 0xff0000) >> 16, (rgb & 0xff00) >> 8, (rgb & 0xff)); + boolean isTrue = ArrayUtils.contains(removeRgb, hex) || + areColorsWithinTolerance(hexToRgb(mainColor), new Color(Integer.parseInt(hex.substring(1), 16)), tolerance); + if (isTrue) { + rgb = override == null ? ((alpha + 1) << 24) | (rgb & 0x00ffffff) : override.getRGB(); + } + image.setRGB(x, y, rgb); + } + } + graphics.drawImage(image, 0, 0, imageIcon.getImageObserver()); + return image; + } + + /** + * 背景移除 + * 图片去底工具 + * 将 "纯色背景的图片" 还原成 "透明背景的图片" + * 将纯色背景的图片转成矢量图 + * 取图片边缘的像素点和获取到的图片主题色作为要替换的背景色 + * 再加入一定的容差值,然后将所有像素点与该颜色进行比较 + * 发现相同则将颜色不透明度设置为0,使颜色完全透明. + * + * @param outputStream 需要进行处理的图片字节数组流 + * @param override 指定替换成的背景颜色 为null时背景为透明 + * @param tolerance 容差值[根据图片的主题色,加入容差值,值的取值范围在0~255之间] + * @return 返回处理好的图片流 + */ + public static BufferedImage backgroundRemoval(ByteArrayOutputStream outputStream, Color override, int tolerance) { + try { + return backgroundRemoval(ImageIO.read(new ByteArrayInputStream(outputStream.toByteArray())), override, tolerance); + } catch (IOException e) { + e.printStackTrace(); + return null; + } + } + + /** + * 获取要删除的 RGB 元素 + * 分别获取图片左上、中上、右上、右中、右下、下中、左下、左中、8个像素点rgb的16进制值 + * + * @param image 图片流 + * @return String数组 包含 各个位置的rgb数值 + */ + private static String[] getRemoveRgb(BufferedImage image) { + // 获取图片流的宽和高 + int width = image.getWidth() - 1; + int height = image.getHeight() - 1; + // 左上 + int leftUpPixel = image.getRGB(1, 1); + String leftUp = ImgUtil.toHex((leftUpPixel & 0xff0000) >> 16, (leftUpPixel & 0xff00) >> 8, (leftUpPixel & 0xff)); + // 上中 + int upMiddlePixel = image.getRGB(width / 2, 1); + String upMiddle = ImgUtil.toHex((upMiddlePixel & 0xff0000) >> 16, (upMiddlePixel & 0xff00) >> 8, (upMiddlePixel & 0xff)); + // 右上 + int rightUpPixel = image.getRGB(width, 1); + String rightUp = ImgUtil.toHex((rightUpPixel & 0xff0000) >> 16, (rightUpPixel & 0xff00) >> 8, (rightUpPixel & 0xff)); + // 右中 + int rightMiddlePixel = image.getRGB(width, height / 2); + String rightMiddle = ImgUtil.toHex((rightMiddlePixel & 0xff0000) >> 16, (rightMiddlePixel & 0xff00) >> 8, (rightMiddlePixel & 0xff)); + // 右下 + int lowerRightPixel = image.getRGB(width, height); + String lowerRight = ImgUtil.toHex((lowerRightPixel & 0xff0000) >> 16, (lowerRightPixel & 0xff00) >> 8, (lowerRightPixel & 0xff)); + // 下中 + int lowerMiddlePixel = image.getRGB(width / 2, height); + String lowerMiddle = ImgUtil.toHex((lowerMiddlePixel & 0xff0000) >> 16, (lowerMiddlePixel & 0xff00) >> 8, (lowerMiddlePixel & 0xff)); + // 左下 + int leftLowerPixel = image.getRGB(1, height); + String leftLower = ImgUtil.toHex((leftLowerPixel & 0xff0000) >> 16, (leftLowerPixel & 0xff00) >> 8, (leftLowerPixel & 0xff)); + // 左中 + int leftMiddlePixel = image.getRGB(1, height / 2); + String leftMiddle = ImgUtil.toHex((leftMiddlePixel & 0xff0000) >> 16, (leftMiddlePixel & 0xff00) >> 8, (leftMiddlePixel & 0xff)); + // 需要删除的RGB元素 + return new String[] {leftUp, upMiddle, rightUp, rightMiddle, lowerRight, lowerMiddle, leftLower, leftMiddle}; + } + + /** + * 十六进制颜色码转RGB颜色值 + * + * @param hex 十六进制颜色码 + * @return 返回 RGB颜色值 + */ + public static Color hexToRgb(String hex) { + return new Color(Integer.parseInt(hex.substring(1), 16)); + } + + /** + * 判断颜色是否在容差范围内 + * 对比两个颜色的相似度,判断这个相似度是否小于 tolerance 容差值 + * + * @param color1 颜色1 + * @param color2 颜色2 + * @param tolerance 容差值 + * @return 返回true:两个颜色在容差值之内 false: 不在 + */ + public static boolean areColorsWithinTolerance(Color color1, Color color2, int tolerance) { + return areColorsWithinTolerance(color1, color2, new Color(tolerance, tolerance, tolerance)); + } + + /** + * 判断颜色是否在容差范围内 + * 对比两个颜色的相似度,判断这个相似度是否小于 tolerance 容差值 + * + * @param color1 颜色1 + * @param color2 颜色2 + * @param tolerance 容差色值 + * @return 返回true:两个颜色在容差值之内 false: 不在 + */ + public static boolean areColorsWithinTolerance(Color color1, Color color2, Color tolerance) { + return (color1.getRed() - color2.getRed() < tolerance.getRed() && color1 + .getRed() - color2.getRed() > -tolerance.getRed()) + && (color1.getBlue() - color2.getBlue() < tolerance + .getBlue() + && color1.getBlue() - color2.getBlue() > -tolerance + .getBlue()) + && (color1.getGreen() - color2.getGreen() < tolerance + .getGreen() + && color1.getGreen() + - color2.getGreen() > -tolerance.getGreen()); + } + + /** + * 获取图片大概的主题色 + * 循环所有的像素点,取出出现次数最多的一个像素点的RGB值 + * + * @param input 图片文件路径 + * @return 返回一个图片的大概的色值 一个16进制的颜色码 + */ + public static String getMainColor(String input) { + return getMainColor(new File(input)); + } + + /** + * 获取图片大概的主题色 + * 循环所有的像素点,取出出现次数最多的一个像素点的RGB值 + * + * @param input 图片文件 + * @return 返回一个图片的大概的色值 一个16进制的颜色码 + */ + public static String getMainColor(File input) { + try { + return getMainColor(ImageIO.read(input)); + } catch (IOException e) { + e.printStackTrace(); + } + return ""; + } + + /** + * 获取图片大概的主题色 + * 循环所有的像素点,取出出现次数最多的一个像素点的RGB值 + * + * @param bufferedImage 图片流 + * @return 返回一个图片的大概的色值 一个16进制的颜色码 + */ + public static String getMainColor(BufferedImage bufferedImage) { + if (bufferedImage == null) { + throw new IllegalArgumentException("图片流是空的"); + } + + // 存储图片的所有RGB元素 + List list = new ArrayList<>(); + for (int y = bufferedImage.getMinY(); y < bufferedImage.getHeight(); y++) { + for (int x = bufferedImage.getMinX(); x < bufferedImage.getWidth(); x++) { + int pixel = bufferedImage.getRGB(x, y); + list.add(((pixel & 0xff0000) >> 16) + "-" + ((pixel & 0xff00) >> 8) + "-" + (pixel & 0xff)); + } + } + + final Map map = new HashMap<>(list.size(), 1); + for (String string : list) { + Integer integer = map.get(string); + if (integer == null) { + integer = 1; + } else { + integer++; + } + map.put(string, integer); + } + String max = StringTools.EMPTY; + long num = 0; + for (Map.Entry entry : map.entrySet()) { + String key = entry.getKey(); + Integer temp = entry.getValue(); + if (StringTools.isBlank(max) || temp > num) { + max = key; + num = temp; + } + } + String[] strings = max.split("-"); + // rgb 的数量只有3个 + int rgbLength = 3; + if (strings.length == rgbLength) { + return ImgUtil.toHex(Integer.parseInt(strings[0]), Integer.parseInt(strings[1]), + Integer.parseInt(strings[2])); + } + return StringTools.EMPTY; + } + + // -------------------------------------------------------------------------- private + + /** + * 文件类型验证 + * 根据给定文件类型数据,验证给定文件类型. + * + * @param input 需要进行验证的文件 + * @param imagesType 文件包含的类型数组 + * @return 返回布尔值 false:给定文件的文件类型在文件数组中 true:给定文件的文件类型 不在给定数组中。 + */ + private static boolean fileTypeValidation(File input, String[] imagesType) { + if (!input.exists()) { + throw new IllegalArgumentException("给定文件为空"); + } + // 获取图片类型 TODO 这里应该读文件头获取才准确 + String type = FileNameUtil.extName(input); + // 类型对比 + if (!ArrayUtils.contains(imagesType, type)) { + throw new IllegalArgumentException(StringTools.format("文件类型{}不支持", type)); + } + return false; + } +} diff --git a/src/main/java/com/luna/common/img/ColorUtil.java b/src/main/java/com/luna/common/img/ColorUtil.java new file mode 100644 index 000000000..c0e30a582 --- /dev/null +++ b/src/main/java/com/luna/common/img/ColorUtil.java @@ -0,0 +1,263 @@ +package com.luna.common.img; + +import com.google.common.base.Splitter; +import com.luna.common.reflect.ConvertUtil; +import com.luna.common.sensitive.Convert; +import com.luna.common.text.StringTools; +import com.luna.common.utils.ObjectUtils; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.util.*; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +/** + * 颜色工具类 + * + * @since 5.8.7 + */ +public class ColorUtil { + + /** + * RGB颜色范围上限 + */ + private static final int RGB_COLOR_BOUND = 256; + + /** + * Color对象转16进制表示,例如#fcf6d6 + * + * @param color {@link Color} + * @return 16进制的颜色值,例如#fcf6d6 + * @since 4.1.14 + */ + public static String toHex(Color color) { + return toHex(color.getRed(), color.getGreen(), color.getBlue()); + } + + /** + * RGB颜色值转换成十六进制颜色码 + * + * @param r 红(R) + * @param g 绿(G) + * @param b 蓝(B) + * @return 返回字符串形式的 十六进制颜色码 如 + */ + public static String toHex(int r, int g, int b) { + // rgb 小于 255 + if (r < 0 || r > 255 || g < 0 || g > 255 || b < 0 || b > 255) { + throw new IllegalArgumentException("RGB must be 0~255!"); + } + return String.format("#%02X%02X%02X", r, g, b); + } + + /** + * 将颜色值转换成具体的颜色类型 汇集了常用的颜色集,支持以下几种形式: + * + *
+     * 1. 颜色的英文名(大小写皆可)
+     * 2. 16进制表示,例如:#fcf6d6或者$fcf6d6
+     * 3. RGB形式,例如:13,148,252
+     * 
+ *

+ * 方法来自: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 rgb = Splitter.on(",").splitToList(colorName); + if (3 == rgb.size()) { + final Integer r = ConvertUtil.toInt(rgb.get(0)); + final Integer g = ConvertUtil.toInt(rgb.get(1)); + final Integer b = ConvertUtil.toInt(rgb.get(2)); + if (!ObjectUtils.hasNull(r, g, b)) { + return new Color(r, g, b); + } + } else { + return null; + } + } + return null; + } + + /** + * 获取一个RGB值对应的颜色 + * + * @param rgb RGB值 + * @return {@link Color} + * @since 4.1.14 + */ + public static Color getColor(int rgb) { + return new Color(rgb); + } + + /** + * 16进制的颜色值转换为Color对象,例如#fcf6d6 + * + * @param hex 16进制的颜色值,例如#fcf6d6 + * @return {@link Color} + * @since 4.1.14 + */ + public static Color hexToColor(String hex) { + return getColor(Integer.parseInt(StringTools.removeStart(hex, "#"), 16)); + } + + /** + * 叠加颜色 + * + * @param color1 颜色1 + * @param color2 颜色2 + * @return 叠加后的颜色 + */ + public static Color add(Color color1, Color color2) { + double r1 = color1.getRed(); + double g1 = color1.getGreen(); + double b1 = color1.getBlue(); + double a1 = color1.getAlpha(); + double r2 = color2.getRed(); + double g2 = color2.getGreen(); + double b2 = color2.getBlue(); + double a2 = color2.getAlpha(); + int r = (int)((r1 * a1 / 255 + r2 * a2 / 255) / (a1 / 255 + a2 / 255)); + int g = (int)((g1 * a1 / 255 + g2 * a2 / 255) / (a1 / 255 + a2 / 255)); + int b = (int)((b1 * a1 / 255 + b2 * a2 / 255) / (a1 / 255 + a2 / 255)); + return new Color(r, g, b); + } + + /** + * 生成随机颜色 + * + * @return 随机颜色 + * @since 3.1.2 + */ + public static Color randomColor() { + return randomColor(null); + } + + /** + * 生成随机颜色 + * + * @param random 随机对象 {@link Random} + * @return 随机颜色 + * @since 3.1.2 + */ + public static Color randomColor(Random random) { + if (null == random) { + random = ThreadLocalRandom.current(); + } + return new Color(random.nextInt(RGB_COLOR_BOUND), random.nextInt(RGB_COLOR_BOUND), random.nextInt(RGB_COLOR_BOUND)); + } + + /** + * 获取给定图片的主色调,背景填充用 + * + * @param image {@link BufferedImage} + * @param rgbFilters 过滤多种颜色 + * @return {@link String} #ffffff + * @since 5.6.7 + */ + public static String getMainColor(BufferedImage image, int[]... rgbFilters) { + int r, g, b; + Map countMap = new HashMap<>(); + int width = image.getWidth(); + int height = image.getHeight(); + int minx = image.getMinX(); + int miny = image.getMinY(); + for (int i = minx; i < width; i++) { + for (int j = miny; j < height; j++) { + int pixel = image.getRGB(i, j); + r = (pixel & 0xff0000) >> 16; + g = (pixel & 0xff00) >> 8; + b = (pixel & 0xff); + if (matchFilters(r, g, b, rgbFilters)) { + continue; + } + countMap.merge(r + "-" + g + "-" + b, 1L, Long::sum); + } + } + String maxColor = null; + long maxCount = 0; + for (Map.Entry entry : countMap.entrySet()) { + String key = entry.getKey(); + Long count = entry.getValue(); + if (count > maxCount) { + maxColor = key; + maxCount = count; + } + } + final String[] splitRgbStr = StringTools.split(maxColor, '-'); + String rHex = Integer.toHexString(Integer.parseInt(splitRgbStr[0])); + String gHex = Integer.toHexString(Integer.parseInt(splitRgbStr[1])); + String bHex = Integer.toHexString(Integer.parseInt(splitRgbStr[2])); + rHex = rHex.length() == 1 ? "0" + rHex : rHex; + gHex = gHex.length() == 1 ? "0" + gHex : gHex; + bHex = bHex.length() == 1 ? "0" + bHex : bHex; + return "#" + rHex + gHex + bHex; + } + + /** + * 给定RGB是否匹配过滤器中任何一个RGB颜色 + * + * @param r R + * @param g G + * @param b B + * @param rgbFilters 颜色过滤器 + * @return 是否匹配 + */ + private static boolean matchFilters(int r, int g, int b, int[]... rgbFilters) { + if (rgbFilters != null && rgbFilters.length > 0) { + for (int[] rgbFilter : rgbFilters) { + if (r == rgbFilter[0] && g == rgbFilter[1] && b == rgbFilter[2]) { + return true; + } + } + } + return false; + } +} diff --git a/src/main/java/com/luna/common/img/FontUtil.java b/src/main/java/com/luna/common/img/FontUtil.java new file mode 100755 index 000000000..19293b060 --- /dev/null +++ b/src/main/java/com/luna/common/img/FontUtil.java @@ -0,0 +1,106 @@ +package com.luna.common.img; + +import com.luna.common.exception.UtilException; + +import java.awt.*; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +/** + * AWT中字体相关工具类 + * + * @author looly + * @since 5.3.6 + */ +public class FontUtil { + + /** + * 创建默认字体 + * + * @return 默认字体 + */ + public static Font createFont() { + return new Font(null); + } + + /** + * 创建SansSerif字体 + * + * @param size 字体大小 + * @return 字体 + */ + public static Font createSansSerifFont(int size) { + return createFont(Font.SANS_SERIF, size); + } + + /** + * 创建指定名称的字体 + * + * @param name 字体名称 + * @param size 字体大小 + * @return 字体 + */ + public static Font createFont(String name, int size) { + return new Font(name, Font.PLAIN, size); + } + + /** + * 根据文件创建字体
+ * 首先尝试创建{@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 iterator = ImageIO.getImageReadersByFormatName(type); + if (iterator.hasNext()) { + return iterator.next(); + } + return null; + } + + /** + * 从文件中读取图片,请使用绝对路径,使用相对路径会相对于ClassPath + * + * @param imageFilePath 图片文件路径 + * @return 图片 + * @since 4.1.15 + */ + public static BufferedImage read(String imageFilePath) { + return read(FileTools.file(imageFilePath)); + } + + /** + * 从文件中读取图片 + * + * @param imageFile 图片文件 + * @return 图片 + * @since 3.2.2 + */ + public static BufferedImage read(File imageFile) { + BufferedImage result; + try { + result = ImageIO.read(imageFile); + } catch (IOException e) { + throw new RuntimeException(e); + } + + if (null == result) { + throw new IllegalArgumentException("Image type of file [" + imageFile.getName() + "] is not supported!"); + } + + return result; + } + + /** + * 从URL中获取或读取图片对象 + * + * @param url URL + * @return {@link Image} + * @since 5.5.8 + */ + public static Image getImage(URL url) { + return Toolkit.getDefaultToolkit().getImage(url); + } + + /** + * 从流中读取图片 + * + * @param imageStream 图片文件 + * @return 图片 + * @since 3.2.2 + */ + public static BufferedImage read(InputStream imageStream) { + BufferedImage result; + try { + result = ImageIO.read(imageStream); + } catch (IOException e) { + throw new RuntimeException(e); + } + + if (null == result) { + throw new IllegalArgumentException("Image type is not supported!"); + } + + return result; + } + + /** + * 从图片流中读取图片 + * + * @param imageStream 图片文件 + * @return 图片 + * @since 3.2.2 + */ + public static BufferedImage read(ImageInputStream imageStream) { + BufferedImage result; + try { + result = ImageIO.read(imageStream); + } catch (IOException e) { + throw new RuntimeException(e); + } + + if (null == result) { + throw new IllegalArgumentException("Image type is not supported!"); + } + + return result; + } + + /** + * 从URL中读取图片 + * + * @param imageUrl 图片文件 + * @return 图片 + * @since 3.2.2 + */ + public static BufferedImage read(URL imageUrl) { + BufferedImage result; + try { + result = ImageIO.read(imageUrl); + } catch (IOException e) { + throw new RuntimeException(e); + } + + if (null == result) { + throw new IllegalArgumentException("Image type of [" + imageUrl + "] is not supported!"); + } + + return result; + } + + /** + * 获取{@link ImageOutputStream} + * + * @param out {@link OutputStream} + * @return {@link ImageOutputStream} + * @throws RuntimeException IO异常 + * @since 3.1.2 + */ + public static ImageOutputStream getImageOutputStream(OutputStream out) throws RuntimeException { + ImageOutputStream result; + try { + result = ImageIO.createImageOutputStream(out); + } catch (IOException e) { + throw new RuntimeException(e); + } + + if (null == result) { + throw new IllegalArgumentException("Image type is not supported!"); + } + + return result; + } + + /** + * 获取{@link ImageOutputStream} + * + * @param outFile {@link File} + * @return {@link ImageOutputStream} + * @throws RuntimeException IO异常 + * @since 3.2.2 + */ + public static ImageOutputStream getImageOutputStream(File outFile) throws RuntimeException { + ImageOutputStream result; + try { + result = ImageIO.createImageOutputStream(outFile); + } catch (IOException e) { + throw new RuntimeException(e); + } + + if (null == result) { + throw new IllegalArgumentException("Image type of file [" + outFile.getName() + "] is not supported!"); + } + + return result; + } + + /** + * 获取{@link ImageInputStream} + * + * @param in {@link InputStream} + * @return {@link ImageInputStream} + * @throws RuntimeException IO异常 + * @since 3.1.2 + */ + public static ImageInputStream getImageInputStream(InputStream in) throws RuntimeException { + ImageOutputStream result; + try { + result = ImageIO.createImageOutputStream(in); + } catch (IOException e) { + throw new RuntimeException(e); + } + + if (null == result) { + throw new IllegalArgumentException("Image type is not supported!"); + } + + return result; + } + + /** + * 根据给定的Image对象和格式获取对应的{@link ImageWriter},如果未找到合适的Writer,返回null + * + * @param img {@link Image} + * @param formatName 图片格式,例如"jpg"、"png" + * @return {@link ImageWriter} + * @since 4.3.2 + */ + public static ImageWriter getWriter(Image img, String formatName) { + final ImageTypeSpecifier type = ImageTypeSpecifier.createFromRenderedImage(toBufferedImage(img, formatName)); + final Iterator iter = ImageIO.getImageWriters(type, formatName); + return iter.hasNext() ? iter.next() : null; + } + + /** + * 根据给定的图片格式或者扩展名获取{@link ImageWriter},如果未找到合适的Writer,返回null + * + * @param formatName 图片格式或扩展名,例如"jpg"、"png" + * @return {@link ImageWriter} + * @since 4.3.2 + */ + public static ImageWriter getWriter(String formatName) { + ImageWriter writer = null; + Iterator iter = ImageIO.getImageWritersByFormatName(formatName); + if (iter.hasNext()) { + writer = iter.next(); + } + if (null == writer) { + // 尝试扩展名获取 + iter = ImageIO.getImageWritersBySuffix(formatName); + if (iter.hasNext()) { + writer = iter.next(); + } + } + return writer; + } + + // -------------------------------------------------------------------------------------------------------------------- + // Color + + /** + * Color对象转16进制表示,例如#fcf6d6 + * + * @param color {@link Color} + * @return 16进制的颜色值,例如#fcf6d6 + * @see ColorUtil#toHex(Color) + * @since 4.1.14 + */ + public static String toHex(Color color) { + return ColorUtil.toHex(color); + } + + /** + * RGB颜色值转换成十六进制颜色码 + * + * @param r 红(R) + * @param g 绿(G) + * @param b 蓝(B) + * @return 返回字符串形式的 十六进制颜色码 + * @see ColorUtil#toHex(int, int, int) + */ + public static String toHex(int r, int g, int b) { + return ColorUtil.toHex(r, g, b); + } + + /** + * 16进制的颜色值转换为Color对象,例如#fcf6d6 + * + * @param hex 16进制的颜色值,例如#fcf6d6 + * @return {@link Color} + * @since 4.1.14 + */ + public static Color hexToColor(String hex) { + return ColorUtil.hexToColor(hex); + } + + /** + * 获取一个RGB值对应的颜色 + * + * @param rgb RGB值 + * @return {@link Color} + * @see ColorUtil#getColor(int) + * @since 4.1.14 + */ + public static Color getColor(int rgb) { + return ColorUtil.getColor(rgb); + } + + /** + * 将颜色值转换成具体的颜色类型 汇集了常用的颜色集,支持以下几种形式: + * + *

+     * 1. 颜色的英文名(大小写皆可)
+     * 2. 16进制表示,例如:#fcf6d6或者$fcf6d6
+     * 3. RGB形式,例如:13,148,252
+     * 
+ *

+ * 方法来自: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); + } + + /** + * 图片颜色转换
+ * 可以使用灰度 (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 URI的格式规范: + * + *

+     *     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 形式存储的颜色。
+ *
    + *
  • L: 亮度
  • + *
  • a: 正数代表红色,负端代表绿色
  • + *
  • b: 正数代表黄色,负端代表蓝色
  • + *
+ * + * @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();
+ * 
+ *

+ * 来自: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: + * + *

+ * {@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
+ * }
+ * }
+ * 
+ *

+ * 来自: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 frames; // frames read from current file + protected int frameCount; + + /** + * Gets display duration for specified frame. + * + * @param n int index of frame + * @return delay in milliseconds + */ + public int getDelay(int n) { + // + delay = -1; + if ((n >= 0) && (n < frameCount)) { + delay = frames.get(n).delay; + } + return delay; + } + + /** + * Gets the number of frames read from file. + * + * @return frame count + */ + public int getFrameCount() { + return frameCount; + } + + /** + * Gets the first (or only) image read. + * + * @return BufferedImage containing first frame, or null if none. + */ + public BufferedImage getImage() { + return getFrame(0); + } + + /** + * Gets the "Netscape" iteration count, if any. + * A count of 0 means repeat indefinitiely. + * + * @return iteration count if one was specified, else 1. + */ + public int getLoopCount() { + return loopCount; + } + + /** + * Creates new frame image from current data (and previous + * frames as specified by their disposition codes). + */ + protected void setPixels() { + // expose destination image's pixels as int array + int[] dest = + ((DataBufferInt)image.getRaster().getDataBuffer()).getData(); + + // fill in starting image contents based on last image's dispose code + if (lastDispose > 0) { + if (lastDispose == 3) { + // use image before last + int n = frameCount - 2; + if (n > 0) { + lastImage = getFrame(n - 1); + } else { + lastImage = null; + } + } + + if (lastImage != null) { + int[] prev = + ((DataBufferInt)lastImage.getRaster().getDataBuffer()).getData(); + System.arraycopy(prev, 0, dest, 0, width * height); + // copy pixels + + if (lastDispose == 2) { + // fill last image rect area with background color + Graphics2D g = image.createGraphics(); + Color c; + if (transparency) { + c = new Color(0, 0, 0, 0); // assume background is transparent + } else { + c = new Color(lastBgColor); // use given background color + } + g.setColor(c); + g.setComposite(AlphaComposite.Src); // replace area + g.fill(lastRect); + g.dispose(); + } + } + } + + // copy each source line to the appropriate place in the destination + int pass = 1; + int inc = 8; + int iline = 0; + for (int i = 0; i < ih; i++) { + int line = i; + if (interlace) { + if (iline >= ih) { + pass++; + switch (pass) { + case 2: + iline = 4; + break; + case 3: + iline = 2; + inc = 4; + break; + case 4: + iline = 1; + inc = 2; + } + } + line = iline; + iline += inc; + } + line += iy; + if (line < height) { + int k = line * width; + int dx = k + ix; // start of line in dest + int dlim = dx + iw; // end of dest line + if ((k + width) < dlim) { + dlim = k + width; // past dest edge + } + int sx = i * iw; // start of line in source + while (dx < dlim) { + // map color and insert in destination + int index = ((int)pixels[sx++]) & 0xff; + int c = act[index]; + if (c != 0) { + dest[dx] = c; + } + dx++; + } + } + } + } + + /** + * Gets the image contents of frame n. + * + * @param n frame + * @return BufferedImage + */ + public BufferedImage getFrame(int n) { + BufferedImage im = null; + if ((n >= 0) && (n < frameCount)) { + im = frames.get(n).image; + } + return im; + } + + /** + * Gets image size. + * + * @return GIF image dimensions + */ + public Dimension getFrameSize() { + return new Dimension(width, height); + } + + /** + * Reads GIF image from stream + * + * @param is BufferedInputStream containing GIF file. + * @return read status code (0 = no errors) + */ + public int read(BufferedInputStream is) { + init(); + if (is != null) { + in = is; + readHeader(); + if (false == err()) { + readContents(); + if (frameCount < 0) { + status = STATUS_FORMAT_ERROR; + } + } + } else { + status = STATUS_OPEN_ERROR; + } + IoUtil.close(is); + return status; + } + + /** + * Reads GIF image from stream + * + * @param is InputStream containing GIF file. + * @return read status code (0 = no errors) + */ + public int read(InputStream is) { + init(); + if (is != null) { + if (!(is instanceof BufferedInputStream)) + is = new BufferedInputStream(is); + in = (BufferedInputStream)is; + readHeader(); + if (!err()) { + readContents(); + if (frameCount < 0) { + status = STATUS_FORMAT_ERROR; + } + } + } else { + status = STATUS_OPEN_ERROR; + } + IoUtil.close(is); + return status; + } + + /** + * Reads GIF file from specified file/URL source + * (URL assumed if name contains ":/" or "file:") + * + * @param name String containing source + * @return read status code (0 = no errors) + */ + public int read(String name) { + status = STATUS_OK; + try { + name = name.trim().toLowerCase(); + if ((name.contains("file:")) || + (name.indexOf(":/") > 0)) { + URL url = new URL(name); + in = new BufferedInputStream(url.openStream()); + } else { + in = new BufferedInputStream(Files.newInputStream(Paths.get(name))); + } + status = read(in); + } catch (IOException e) { + status = STATUS_OPEN_ERROR; + } + + return status; + } + + /** + * Decodes LZW image data into pixel array. + * Adapted from John Cristy's ImageMagick. + */ + protected void decodeImageData() { + int NullCode = -1; + int npix = iw * ih; + int available, + clear, + code_mask, + code_size, + end_of_information, + in_code, + old_code, + bits, + code, + count, + i, + datum, + data_size, + first, + top, + bi, + pi; + + if ((pixels == null) || (pixels.length < npix)) { + pixels = new byte[npix]; // allocate new pixel array + } + if (prefix == null) + prefix = new short[MAX_STACK_SIZE]; + if (suffix == null) + suffix = new byte[MAX_STACK_SIZE]; + if (pixelStack == null) + pixelStack = new byte[MAX_STACK_SIZE + 1]; + + // Initialize GIF data stream decoder. + + data_size = read(); + clear = 1 << data_size; + end_of_information = clear + 1; + available = clear + 2; + old_code = NullCode; + code_size = data_size + 1; + code_mask = (1 << code_size) - 1; + for (code = 0; code < clear; code++) { + prefix[code] = 0; + suffix[code] = (byte)code; + } + + // Decode GIF pixel stream. + + datum = bits = count = first = top = pi = bi = 0; + + for (i = 0; i < npix;) { + if (top == 0) { + if (bits < code_size) { + // Load bytes until there are enough bits for a code. + if (count == 0) { + // Read a new data block. + count = readBlock(); + if (count <= 0) + break; + bi = 0; + } + datum += (((int)block[bi]) & 0xff) << bits; + bits += 8; + bi++; + count--; + continue; + } + + // Get the next code. + + code = datum & code_mask; + datum >>= code_size; + bits -= code_size; + + // Interpret the code + + if ((code > available) || (code == end_of_information)) + break; + if (code == clear) { + // Reset decoder. + code_size = data_size + 1; + code_mask = (1 << code_size) - 1; + available = clear + 2; + old_code = NullCode; + continue; + } + if (old_code == NullCode) { + pixelStack[top++] = suffix[code]; + old_code = code; + first = code; + continue; + } + in_code = code; + if (code == available) { + pixelStack[top++] = (byte)first; + code = old_code; + } + while (code > clear) { + pixelStack[top++] = suffix[code]; + code = prefix[code]; + } + first = ((int)suffix[code]) & 0xff; + + // Add a new string to the string table, + + if (available >= MAX_STACK_SIZE) { + pixelStack[top++] = (byte)first; + continue; + } + pixelStack[top++] = (byte)first; + prefix[available] = (short)old_code; + suffix[available] = (byte)first; + available++; + if (((available & code_mask) == 0) + && (available < MAX_STACK_SIZE)) { + code_size++; + code_mask += available; + } + old_code = in_code; + } + + // Pop a pixel off the pixel stack. + + top--; + pixels[pi++] = pixelStack[top]; + i++; + } + + for (i = pi; i < npix; i++) { + pixels[i] = 0; // clear missing pixels + } + + } + + /** + * Returns true if an error was encountered during reading/decoding + * + * @return true if an error was encountered during reading/decoding + */ + protected boolean err() { + return status != STATUS_OK; + } + + /** + * Initializes or re-initializes reader + */ + protected void init() { + status = STATUS_OK; + frameCount = 0; + frames = new ArrayList<>(); + gct = null; + lct = null; + } + + /** + * Reads a single byte from the input stream. + * + * @return single byte + */ + protected int read() { + int curByte = 0; + try { + curByte = in.read(); + } catch (IOException e) { + status = STATUS_FORMAT_ERROR; + } + return curByte; + } + + /** + * Reads next variable length block from input. + * + * @return number of bytes stored in "buffer" + */ + protected int readBlock() { + blockSize = read(); + int n = 0; + if (blockSize > 0) { + try { + int count; + while (n < blockSize) { + count = in.read(block, n, blockSize - n); + if (count == -1) + break; + n += count; + } + } catch (IOException e) { + // ignore + } + + if (n < blockSize) { + status = STATUS_FORMAT_ERROR; + } + } + return n; + } + + /** + * Reads color table as 256 RGB integer values + * + * @param ncolors int number of colors to read + * @return int array containing 256 colors (packed ARGB with full alpha) + */ + protected int[] readColorTable(int ncolors) { + int nbytes = 3 * ncolors; + int[] tab = null; + byte[] c = new byte[nbytes]; + int n = 0; + try { + n = in.read(c); + } catch (IOException e) { + // ignore + } + if (n < nbytes) { + status = STATUS_FORMAT_ERROR; + } else { + tab = new int[256]; // max size to avoid bounds checks + int i = 0; + int j = 0; + while (i < ncolors) { + int r = ((int)c[j++]) & 0xff; + int g = ((int)c[j++]) & 0xff; + int b = ((int)c[j++]) & 0xff; + tab[i++] = 0xff000000 | (r << 16) | (g << 8) | b; + } + } + return tab; + } + + /** + * Main file parser. Reads GIF content blocks. + */ + protected void readContents() { + // read GIF file content blocks + boolean done = false; + while (!(done || err())) { + int code = read(); + switch (code) { + + case 0x2C: // image separator + readImage(); + break; + + case 0x21: // extension + code = read(); + switch (code) { + case 0xf9: // graphics control extension + readGraphicControlExt(); + break; + + case 0xff: // application extension + readBlock(); + final StringBuilder app = new StringBuilder(); + for (int i = 0; i < 11; i++) { + app.append((char)block[i]); + } + if ("NETSCAPE2.0".contentEquals(app)) { + readNetscapeExt(); + } else { + skip(); // don't care + } + break; + + default: // uninteresting extension + skip(); + } + break; + + case 0x3b: // terminator + done = true; + break; + + case 0x00: // bad byte, but keep going and see what happens + break; + + default: + status = STATUS_FORMAT_ERROR; + } + } + } + + /** + * Reads Graphics Control Extension values + */ + protected void readGraphicControlExt() { + read(); // block size + int packed = read(); // packed fields + dispose = (packed & 0x1c) >> 2; // disposal method + if (dispose == 0) { + dispose = 1; // elect to keep old image if discretionary + } + transparency = (packed & 1) != 0; + delay = readShort() * 10; // delay in milliseconds + transIndex = read(); // transparent color index + read(); // block terminator + } + + /** + * Reads GIF file header information. + */ + protected void readHeader() { + final StringBuilder id = new StringBuilder(); + for (int i = 0; i < 6; i++) { + id.append((char)read()); + } + if (false == id.toString().startsWith("GIF")) { + status = STATUS_FORMAT_ERROR; + return; + } + + readLSD(); + if (gctFlag && !err()) { + gct = readColorTable(gctSize); + bgColor = gct[bgIndex]; + } + } + + /** + * Reads next frame image + */ + protected void readImage() { + ix = readShort(); // (sub)image position & size + iy = readShort(); + iw = readShort(); + ih = readShort(); + + int packed = read(); + lctFlag = (packed & 0x80) != 0; // 1 - local color table flag + interlace = (packed & 0x40) != 0; // 2 - interlace flag + // 3 - sort flag + // 4-5 - reserved + lctSize = 2 << (packed & 7); // 6-8 - local color table size + + if (lctFlag) { + lct = readColorTable(lctSize); // read table + act = lct; // make local table active + } else { + act = gct; // make global table active + if (bgIndex == transIndex) + bgColor = 0; + } + int save = 0; + if (transparency) { + save = act[transIndex]; + act[transIndex] = 0; // set transparent color if specified + } + + if (act == null) { + status = STATUS_FORMAT_ERROR; // no color table defined + } + + if (err()) + return; + + decodeImageData(); // decode pixel data + skip(); + + if (err()) + return; + + frameCount++; + + // create new image to receive frame data + image = + new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB_PRE); + + setPixels(); // transfer pixel data to image + + frames.add(new GifFrame(image, delay)); // add image to frame list + + if (transparency) { + act[transIndex] = save; + } + resetFrame(); + + } + + /** + * Reads Logical Screen Descriptor + */ + protected void readLSD() { + + // logical screen size + width = readShort(); + height = readShort(); + + // packed fields + int packed = read(); + gctFlag = (packed & 0x80) != 0; // 1 : global color table flag + // 2-4 : color resolution + // 5 : gct sort flag + gctSize = 2 << (packed & 7); // 6-8 : gct size + + bgIndex = read(); // background color index + pixelAspect = read(); // pixel aspect ratio + } + + /** + * Reads Netscape extenstion to obtain iteration count + */ + protected void readNetscapeExt() { + do { + readBlock(); + if (block[0] == 1) { + // loop count sub-block + int b1 = ((int)block[1]) & 0xff; + int b2 = ((int)block[2]) & 0xff; + loopCount = (b2 << 8) | b1; + } + } while ((blockSize > 0) && !err()); + } + + /** + * Reads next 16-bit value, LSB first + * + * @return next 16-bit value + */ + protected int readShort() { + // read 16-bit value, LSB first + return read() | (read() << 8); + } + + /** + * Resets frame state for reading next image. + */ + protected void resetFrame() { + lastDispose = dispose; + lastRect = new Rectangle(ix, iy, iw, ih); + lastImage = image; + lastBgColor = bgColor; + lct = null; + } + + /** + * Skips variable length blocks up to and including + * next zero length block. + */ + protected void skip() { + do { + readBlock(); + } while ((blockSize > 0) && !err()); + } + + static class GifFrame { + public BufferedImage image; + public int delay; + + public GifFrame(BufferedImage im, int del) { + image = im; + delay = del; + } + } +} diff --git a/src/main/java/com/luna/common/img/gif/LZWEncoder.java b/src/main/java/com/luna/common/img/gif/LZWEncoder.java new file mode 100755 index 000000000..49a43bd66 --- /dev/null +++ b/src/main/java/com/luna/common/img/gif/LZWEncoder.java @@ -0,0 +1,306 @@ +package com.luna.common.img.gif; + +import java.io.IOException; +import java.io.OutputStream; + +//============================================================================== +// Adapted from Jef Poskanzer's Java port by way of J. M. G. Elliott. +// K Weiner 12/00 + +class LZWEncoder { + + private static final int EOF = -1; + + private final int imgW; + private final int imgH; + private final byte[] pixAry; + private final int initCodeSize; + + private int remaining; + private int curPixel; + + // GIFCOMPR.C - GIF Image compression routines + // + // Lempel-Ziv compression based on 'compress'. GIF modifications by + // David Rowley (mgardi@watdcsu.waterloo.edu) + + // General DEFINEs + + static final int BITS = 12; + + static final int HSIZE = 5003; // 80% occupancy + + // GIF Image compression - modified 'compress' + // + // Based on: compress.c - File compression ala IEEE Computer, June 1984. + // + // By Authors: Spencer W. Thomas (decvax!harpo!utah-cs!utah-gr!thomas) + // Jim McKie (decvax!mcvax!jim) + // Steve Davies (decvax!vax135!petsd!peora!srd) + // Ken Turkowski (decvax!decwrl!turtlevax!ken) + // James A. Woods (decvax!ihnp4!ames!jaw) + // Joe Orost (decvax!vax135!petsd!joe) + + int n_bits; // number of bits/code + int maxbits = BITS; // user settable max # bits/code + int maxcode; // maximum code, given n_bits + int maxmaxcode = 1 << BITS; // should NEVER generate this code + + int[] htab = new int[HSIZE]; + int[] codetab = new int[HSIZE]; + + int hsize = HSIZE; // for dynamic table sizing + + int free_ent = 0; // first unused entry + + // block compression parameters -- after all codes are used up, + // and compression rate changes, start over. + boolean clear_flg = false; + + // Algorithm: use open addressing double hashing (no chaining) on the + // prefix code / next character combination. We do a variant of Knuth's + // algorithm D (vol. 3, sec. 6.4) along with G. Knott's relatively-prime + // secondary probe. Here, the modular division first probe is gives way + // to a faster exclusive-or manipulation. Also do block compression with + // an adaptive reset, whereby the code table is cleared when the compression + // ratio decreases, but after the table fills. The variable-length output + // codes are re-sized at this point, and a special CLEAR code is generated + // for the decompressor. Late addition: construct the table according to + // file size for noticeable speed improvement on small files. Please direct + // questions about this implementation to ames!jaw. + + int g_init_bits; + + int ClearCode; + int EOFCode; + + // output + // + // Output the given code. + // Inputs: + // code: A n_bits-bit integer. If == -1, then EOF. This assumes + // that n_bits =< wordsize - 1. + // Outputs: + // Outputs code to the file. + // Assumptions: + // Chars are 8 bits long. + // Algorithm: + // Maintain a BITS character long buffer (so that 8 codes will + // fit in it exactly). Use the VAX insv instruction to insert each + // code in turn. When the buffer fills up empty it and start over. + + int cur_accum = 0; + int cur_bits = 0; + + final int[] masks = + { + 0x0000, + 0x0001, + 0x0003, + 0x0007, + 0x000F, + 0x001F, + 0x003F, + 0x007F, + 0x00FF, + 0x01FF, + 0x03FF, + 0x07FF, + 0x0FFF, + 0x1FFF, + 0x3FFF, + 0x7FFF, + 0xFFFF}; + + // Number of characters so far in this 'packet' + int a_count; + + // Define the storage for the packet accumulator + byte[] accum = new byte[256]; + + // ---------------------------------------------------------------------------- + LZWEncoder(int width, int height, byte[] pixels, int color_depth) { + imgW = width; + imgH = height; + pixAry = pixels; + initCodeSize = Math.max(2, color_depth); + } + + // Add a character to the end of the current packet, and if it is 254 + // characters, flush the packet to disk. + void char_out(byte c, OutputStream outs) throws IOException { + accum[a_count++] = c; + if (a_count >= 254) + flush_char(outs); + } + + // Clear out the hash table + + // table clear for block compress + void cl_block(OutputStream outs) throws IOException { + cl_hash(hsize); + free_ent = ClearCode + 2; + clear_flg = true; + + output(ClearCode, outs); + } + + // reset code table + void cl_hash(int hsize) { + for (int i = 0; i < hsize; ++i) + htab[i] = -1; + } + + void compress(int init_bits, OutputStream outs) throws IOException { + int fcode; + int i /* = 0 */; + int c; + int ent; + int disp; + int hsize_reg; + int hshift; + + // Set up the globals: g_init_bits - initial number of bits + g_init_bits = init_bits; + + // Set up the necessary values + clear_flg = false; + n_bits = g_init_bits; + maxcode = MAXCODE(n_bits); + + ClearCode = 1 << (init_bits - 1); + EOFCode = ClearCode + 1; + free_ent = ClearCode + 2; + + a_count = 0; // clear packet + + ent = nextPixel(); + + hshift = 0; + for (fcode = hsize; fcode < 65536; fcode *= 2) + ++hshift; + hshift = 8 - hshift; // set hash code range bound + + hsize_reg = hsize; + cl_hash(hsize_reg); // clear hash table + + output(ClearCode, outs); + + outer_loop: + while ((c = nextPixel()) != EOF) { + fcode = (c << maxbits) + ent; + i = (c << hshift) ^ ent; // xor hashing + + if (htab[i] == fcode) { + ent = codetab[i]; + continue; + } else if (htab[i] >= 0) // non-empty slot + { + disp = hsize_reg - i; // secondary hash (after G. Knott) + if (i == 0) + disp = 1; + do { + if ((i -= disp) < 0) + i += hsize_reg; + + if (htab[i] == fcode) { + ent = codetab[i]; + continue outer_loop; + } + } while (htab[i] >= 0); + } + output(ent, outs); + ent = c; + if (free_ent < maxmaxcode) { + codetab[i] = free_ent++; // code -> hashtable + htab[i] = fcode; + } else + cl_block(outs); + } + // Put out the final code. + output(ent, outs); + output(EOFCode, outs); + } + + // ---------------------------------------------------------------------------- + void encode(OutputStream os) throws IOException { + os.write(initCodeSize); // write "initial code size" byte + + remaining = imgW * imgH; // reset navigation variables + curPixel = 0; + + compress(initCodeSize + 1, os); // compress and write the pixel data + + os.write(0); // write block terminator + } + + // Flush the packet to disk, and reset the accumulator + void flush_char(OutputStream outs) throws IOException { + if (a_count > 0) { + outs.write(a_count); + outs.write(accum, 0, a_count); + a_count = 0; + } + } + + final int MAXCODE(int n_bits) { + return (1 << n_bits) - 1; + } + + // ---------------------------------------------------------------------------- + // Return the next pixel from the image + // ---------------------------------------------------------------------------- + private int nextPixel() { + if (remaining == 0) + return EOF; + + --remaining; + + byte pix = pixAry[curPixel++]; + + return pix & 0xff; + } + + void output(int code, OutputStream outs) throws IOException { + cur_accum &= masks[cur_bits]; + + if (cur_bits > 0) + cur_accum |= (code << cur_bits); + else + cur_accum = code; + + cur_bits += n_bits; + + while (cur_bits >= 8) { + char_out((byte)(cur_accum & 0xff), outs); + cur_accum >>= 8; + cur_bits -= 8; + } + + // If the next entry is going to be too big for the code size, + // then increase it, if possible. + if (free_ent > maxcode || clear_flg) { + if (clear_flg) { + maxcode = MAXCODE(n_bits = g_init_bits); + clear_flg = false; + } else { + ++n_bits; + if (n_bits == maxbits) + maxcode = maxmaxcode; + else + maxcode = MAXCODE(n_bits); + } + } + + if (code == EOFCode) { + // At EOF, write the rest of the buffer. + while (cur_bits > 0) { + char_out((byte)(cur_accum & 0xff), outs); + cur_accum >>= 8; + cur_bits -= 8; + } + + flush_char(outs); + } + } +} diff --git a/src/main/java/com/luna/common/img/gif/NeuQuant.java b/src/main/java/com/luna/common/img/gif/NeuQuant.java new file mode 100755 index 000000000..c71b618ca --- /dev/null +++ b/src/main/java/com/luna/common/img/gif/NeuQuant.java @@ -0,0 +1,460 @@ +package com.luna.common.img.gif; + +/* NeuQuant Neural-Net Quantization Algorithm + * ------------------------------------------ + * + * Copyright (c) 1994 Anthony Dekker + * + * NEUQUANT Neural-Net quantization algorithm by Anthony Dekker, 1994. + * See "Kohonen neural networks for optimal colour quantization" + * in "Network: Computation in Neural Systems" Vol. 5 (1994) pp 351-367. + * for a discussion of the algorithm. + * + * Any party obtaining a copy of these files from the author, directly or + * indirectly, is granted, free of charge, a full and unrestricted irrevocable, + * world-wide, paid up, royalty-free, nonexclusive right and license to deal + * in this software and documentation files (the "Software"), including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons who receive + * copies from any such party to do so, with the only requirement being + * that this copyright notice remain intact. + */ + +// Ported to Java 12/00 K Weiner + +/** + * NeuQuant Neural-Net Quantization Algorithm + * + * @author Dekker + */ +public class NeuQuant { + + protected static final int NETSIZE = 256; /* number of colours used */ + + /* four primes near 500 - assume no image has a length so large */ + /* that it is divisible by all four primes */ + protected static final int PRIME1 = 499; + protected static final int PRIME2 = 491; + protected static final int PRIME3 = 487; + protected static final int PRIME4 = 503; + + protected static final int MINPICTUREBYTES = (3 * PRIME4); + /* minimum size for input image */ + + /* Program Skeleton + ---------------- + [select samplefac in range 1..30] + [read image from input file] + pic = (unsigned char*) malloc(3*width*height); + initnet(pic,3*width*height,samplefac); + learn(); + unbiasnet(); + [write output image header, using writecolourmap(f)] + inxbuild(); + write output image using inxsearch(b,g,r) */ + + /* Network Definitions + ------------------- */ + + protected static final int MAXNETPOS = (NETSIZE - 1); + protected static final int NETBIASSHIFT = 4; /* bias for colour values */ + protected static final int NCYCLES = 100; /* no. of learning cycles */ + + /* defs for freq and bias */ + protected static final int INTBIASSHIFT = 16; /* bias for fractions */ + protected static final int INTBIAS = (1 << INTBIASSHIFT); + protected static final int GAMMASHIFT = 10; /* gamma = 1024 */ + protected static final int GAMMA = (1 << GAMMASHIFT); + protected static final int BETASHIFT = 10; + protected static final int BETA = (INTBIAS >> BETASHIFT); /* beta = 1/1024 */ + protected static final int BETAGAMMA = + (INTBIAS << (GAMMASHIFT - BETASHIFT)); + + /* defs for decreasing radius factor */ + protected static final int INITRAD = (NETSIZE >> 3); /* for 256 cols, radius starts */ + protected static final int RADIUSBIASSHIFT = 6; /* at 32.0 biased by 6 bits */ + protected static final int RADIUSBIAS = (1 << RADIUSBIASSHIFT); + protected static final int INITRADIUS = (INITRAD * RADIUSBIAS); /* and decreases by a */ + protected static final int RADIUSDEC = 30; /* factor of 1/30 each cycle */ + + /* defs for decreasing alpha factor */ + protected static final int ALPHABIASSHIFT = 10; /* alpha starts at 1.0 */ + protected static final int INITALPHA = (1 << ALPHABIASSHIFT); + + protected int alphadec; /* biased by 10 bits */ + + /* radbias and alpharadbias used for radpower calculation */ + protected static final int RADBIASSHIFT = 8; + protected static final int RADBIAS = (1 << RADBIASSHIFT); + protected static final int ALPHARADBSHIFT = (ALPHABIASSHIFT + RADBIASSHIFT); + protected static final int ALPHARADBIAS = (1 << ALPHARADBSHIFT); + + /* Types and Global Variables + -------------------------- */ + + protected byte[] thepicture; /* the input image itself */ + protected int lengthcount; /* lengthcount = H*W*3 */ + + protected int samplefac; /* sampling factor 1..30 */ + + // typedef int pixel[4]; /* BGRc */ + protected int[][] network; /* the network itself - [netsize][4] */ + + protected int[] netindex = new int[256]; + /* for network lookup - really 256 */ + + protected int[] bias = new int[NETSIZE]; + /* bias and freq arrays for learning */ + protected int[] freq = new int[NETSIZE]; + protected int[] radpower = new int[INITRAD]; + /* radpower for precomputation */ + + /* Initialise network in range (0,0,0) to (255,255,255) and set parameters + ----------------------------------------------------------------------- */ + public NeuQuant(byte[] thepic, int len, int sample) { + + int i; + int[] p; + + thepicture = thepic; + lengthcount = len; + samplefac = sample; + + network = new int[NETSIZE][]; + for (i = 0; i < NETSIZE; i++) { + network[i] = new int[4]; + p = network[i]; + p[0] = p[1] = p[2] = (i << (NETBIASSHIFT + 8)) / NETSIZE; + freq[i] = INTBIAS / NETSIZE; /* 1/netsize */ + bias[i] = 0; + } + } + + public byte[] colorMap() { + byte[] map = new byte[3 * NETSIZE]; + int[] index = new int[NETSIZE]; + for (int i = 0; i < NETSIZE; i++) + index[network[i][3]] = i; + int k = 0; + for (int i = 0; i < NETSIZE; i++) { + int j = index[i]; + map[k++] = (byte)(network[j][0]); + map[k++] = (byte)(network[j][1]); + map[k++] = (byte)(network[j][2]); + } + return map; + } + + /* Insertion sort of network and building of netindex[0..255] (to do after unbias) + ------------------------------------------------------------------------------- */ + public void inxbuild() { + + int i, j, smallpos, smallval; + int[] p; + int[] q; + int previouscol, startpos; + + previouscol = 0; + startpos = 0; + for (i = 0; i < NETSIZE; i++) { + p = network[i]; + smallpos = i; + smallval = p[1]; /* index on g */ + /* find smallest in i..netsize-1 */ + for (j = i + 1; j < NETSIZE; j++) { + q = network[j]; + if (q[1] < smallval) { /* index on g */ + smallpos = j; + smallval = q[1]; /* index on g */ + } + } + q = network[smallpos]; + /* swap p (i) and q (smallpos) entries */ + if (i != smallpos) { + j = q[0]; + q[0] = p[0]; + p[0] = j; + j = q[1]; + q[1] = p[1]; + p[1] = j; + j = q[2]; + q[2] = p[2]; + p[2] = j; + j = q[3]; + q[3] = p[3]; + p[3] = j; + } + /* smallval entry is now in position i */ + if (smallval != previouscol) { + netindex[previouscol] = (startpos + i) >> 1; + for (j = previouscol + 1; j < smallval; j++) + netindex[j] = i; + previouscol = smallval; + startpos = i; + } + } + netindex[previouscol] = (startpos + MAXNETPOS) >> 1; + for (j = previouscol + 1; j < 256; j++) + netindex[j] = MAXNETPOS; /* really 256 */ + } + + /* Main Learning Loop + ------------------ */ + public void learn() { + + int i, j, b, g, r; + int radius, rad, alpha, step, delta, samplepixels; + byte[] p; + int pix, lim; + + if (lengthcount < MINPICTUREBYTES) + samplefac = 1; + alphadec = 30 + ((samplefac - 1) / 3); + p = thepicture; + pix = 0; + lim = lengthcount; + samplepixels = lengthcount / (3 * samplefac); + delta = samplepixels / NCYCLES; + alpha = INITALPHA; + radius = INITRADIUS; + + rad = radius >> RADIUSBIASSHIFT; + for (i = 0; i < rad; i++) + radpower[i] = + alpha * (((rad * rad - i * i) * RADBIAS) / (rad * rad)); + + // fprintf(stderr,"beginning 1D learning: initial radius=%d\n", rad); + + if (lengthcount < MINPICTUREBYTES) + step = 3; + else if ((lengthcount % PRIME1) != 0) + step = 3 * PRIME1; + else { + if ((lengthcount % PRIME2) != 0) + step = 3 * PRIME2; + else { + if ((lengthcount % PRIME3) != 0) + step = 3 * PRIME3; + else + step = 3 * PRIME4; + } + } + + i = 0; + while (i < samplepixels) { + b = (p[pix] & 0xff) << NETBIASSHIFT; + g = (p[pix + 1] & 0xff) << NETBIASSHIFT; + r = (p[pix + 2] & 0xff) << NETBIASSHIFT; + j = contest(b, g, r); + + altersingle(alpha, j, b, g, r); + if (rad != 0) + alterneigh(rad, j, b, g, r); /* alter neighbours */ + + pix += step; + if (pix >= lim) + pix -= lengthcount; + + i++; + if (delta == 0) + delta = 1; + if (i % delta == 0) { + alpha -= alpha / alphadec; + radius -= radius / RADIUSDEC; + rad = radius >> RADIUSBIASSHIFT; + if (rad <= 1) + rad = 0; + for (j = 0; j < rad; j++) + radpower[j] = + alpha * (((rad * rad - j * j) * RADBIAS) / (rad * rad)); + } + } + // fprintf(stderr,"finished 1D learning: final alpha=%f !\n",((float)alpha)/initalpha); + } + + /* Search for BGR values 0..255 (after net is unbiased) and return colour index + ---------------------------------------------------------------------------- */ + public int map(int b, int g, int r) { + + int i, j, dist, a, bestd; + int[] p; + int best; + + bestd = 1000; /* biggest possible dist is 256*3 */ + best = -1; + i = netindex[g]; /* index on g */ + j = i - 1; /* start at netindex[g] and work outwards */ + + while ((i < NETSIZE) || (j >= 0)) { + if (i < NETSIZE) { + p = network[i]; + dist = p[1] - g; /* inx key */ + if (dist >= bestd) + i = NETSIZE; /* stop iter */ + else { + i++; + if (dist < 0) + dist = -dist; + a = p[0] - b; + if (a < 0) + a = -a; + dist += a; + if (dist < bestd) { + a = p[2] - r; + if (a < 0) + a = -a; + dist += a; + if (dist < bestd) { + bestd = dist; + best = p[3]; + } + } + } + } + if (j >= 0) { + p = network[j]; + dist = g - p[1]; /* inx key - reverse dif */ + if (dist >= bestd) + j = -1; /* stop iter */ + else { + j--; + if (dist < 0) + dist = -dist; + a = p[0] - b; + if (a < 0) + a = -a; + dist += a; + if (dist < bestd) { + a = p[2] - r; + if (a < 0) + a = -a; + dist += a; + if (dist < bestd) { + bestd = dist; + best = p[3]; + } + } + } + } + } + return (best); + } + + public byte[] process() { + learn(); + unbiasnet(); + inxbuild(); + return colorMap(); + } + + /* Unbias network to give byte values 0..255 and record position i to prepare for sort + ----------------------------------------------------------------------------------- */ + public void unbiasnet() { + for (int i = 0; i < NETSIZE; i++) { + network[i][0] >>= NETBIASSHIFT; + network[i][1] >>= NETBIASSHIFT; + network[i][2] >>= NETBIASSHIFT; + network[i][3] = i; /* record colour no */ + } + } + + /* Move adjacent neurons by precomputed alpha*(1-((i-j)^2/[r]^2)) in radpower[|i-j|] + --------------------------------------------------------------------------------- */ + protected void alterneigh(int rad, int i, int b, int g, int r) { + + int j, k, lo, hi, a, m; + int[] p; + + lo = i - rad; + if (lo < -1) + lo = -1; + hi = i + rad; + if (hi > NETSIZE) + hi = NETSIZE; + + j = i + 1; + k = i - 1; + m = 1; + while ((j < hi) || (k > lo)) { + a = radpower[m++]; + if (j < hi) { + p = network[j++]; + try { + p[0] -= (a * (p[0] - b)) / ALPHARADBIAS; + p[1] -= (a * (p[1] - g)) / ALPHARADBIAS; + p[2] -= (a * (p[2] - r)) / ALPHARADBIAS; + } catch (Exception ignored) { + } // prevents 1.3 miscompilation + } + if (k > lo) { + p = network[k--]; + try { + p[0] -= (a * (p[0] - b)) / ALPHARADBIAS; + p[1] -= (a * (p[1] - g)) / ALPHARADBIAS; + p[2] -= (a * (p[2] - r)) / ALPHARADBIAS; + } catch (Exception ignored) { + } + } + } + } + + /* Move neuron i towards biased (b,g,r) by factor alpha + ---------------------------------------------------- */ + protected void altersingle(int alpha, int i, int b, int g, int r) { + + /* alter hit neuron */ + int[] n = network[i]; + n[0] -= (alpha * (n[0] - b)) / INITALPHA; + n[1] -= (alpha * (n[1] - g)) / INITALPHA; + n[2] -= (alpha * (n[2] - r)) / INITALPHA; + } + + /* Search for biased BGR values + ---------------------------- */ + protected int contest(int b, int g, int r) { + + /* finds closest neuron (min dist) and updates freq */ + /* finds best neuron (min dist-bias) and returns position */ + /* for frequently chosen neurons, freq[i] is high and bias[i] is negative */ + /* bias[i] = gamma*((1/netsize)-freq[i]) */ + + int i, dist, a, biasdist, betafreq; + int bestpos, bestbiaspos, bestd, bestbiasd; + int[] n; + + bestd = ~(1 << 31); + bestbiasd = bestd; + bestpos = -1; + bestbiaspos = bestpos; + + for (i = 0; i < NETSIZE; i++) { + n = network[i]; + dist = n[0] - b; + if (dist < 0) + dist = -dist; + a = n[1] - g; + if (a < 0) + a = -a; + dist += a; + a = n[2] - r; + if (a < 0) + a = -a; + dist += a; + if (dist < bestd) { + bestd = dist; + bestpos = i; + } + biasdist = dist - ((bias[i]) >> (INTBIASSHIFT - NETBIASSHIFT)); + if (biasdist < bestbiasd) { + bestbiasd = biasdist; + bestbiaspos = i; + } + betafreq = (freq[i] >> BETASHIFT); + freq[i] -= betafreq; + bias[i] += (betafreq << GAMMASHIFT); + } + freq[bestpos] += BETA; + bias[bestpos] -= BETAGAMMA; + return (bestbiaspos); + } +} diff --git a/src/main/java/com/luna/common/img/gif/package-info.java b/src/main/java/com/luna/common/img/gif/package-info.java new file mode 100755 index 000000000..eace67281 --- /dev/null +++ b/src/main/java/com/luna/common/img/gif/package-info.java @@ -0,0 +1,7 @@ +/** + * GIF处理,来自:https://github.com/rtyley/animated-gif-lib-for-java + * + * @author looly + * + */ +package com.luna.common.img.gif; \ No newline at end of file diff --git a/src/main/java/com/luna/common/img/package-info.java b/src/main/java/com/luna/common/img/package-info.java new file mode 100755 index 000000000..d679cb450 --- /dev/null +++ b/src/main/java/com/luna/common/img/package-info.java @@ -0,0 +1,7 @@ +/** + * 图像处理相关工具类封装 + * + * @author looly + * + */ +package com.luna.common.img; \ No newline at end of file diff --git a/src/main/java/com/luna/common/swing/ClipboardListener.java b/src/main/java/com/luna/common/swing/ClipboardListener.java new file mode 100755 index 000000000..b860663c9 --- /dev/null +++ b/src/main/java/com/luna/common/swing/ClipboardListener.java @@ -0,0 +1,23 @@ +package com.luna.common.swing; + +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.Transferable; + +/** + * 剪贴板监听事件处理接口
+ * 用户通过实现此接口,实现监听剪贴板内容变化 + * + * @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 listenerSet = new LinkedHashSet<>(); + /** 是否正在监听 */ + private boolean isRunning; + + // ---------------------------------------------------------------------------------------------------------- + // Constructor start + /** + * 构造,尝试获取剪贴板内容的次数为10,第二次之后延迟100毫秒 + */ + ClipboardMonitor() { + this(DEFAULT_TRY_COUNT, DEFAULT_DELAY); + } + + /** + * 构造 + * + * @param tryCount 尝试获取剪贴板内容的次数 + * @param delay 响应延迟,当从第二次开始,延迟一定毫秒数等待剪贴板可以获取,当tryCount小于2时无效 + */ + ClipboardMonitor(int tryCount, long delay) { + this(tryCount, delay, ClipboardUtil.getClipboard()); + } + + /** + * 构造 + * + * @param tryCount 尝试获取剪贴板内容的次数 + * @param delay 响应延迟,当从第二次开始,延迟一定毫秒数等待剪贴板可以获取,当tryCount小于2时无效 + * @param clipboard 剪贴板对象 + */ + ClipboardMonitor(int tryCount, long delay, Clipboard clipboard) { + this.tryCount = tryCount; + this.delay = delay; + this.clipboard = clipboard; + } + // ---------------------------------------------------------------------------------------------------------- + // Constructor end + + /** + * 设置重试次数 + * + * @param tryCount 重试次数 + * @return this + */ + public ClipboardMonitor setTryCount(int tryCount) { + this.tryCount = tryCount; + return this; + } + + /** + * 设置重试等待 + * + * @param delay 重试等待 + * @return this + */ + public ClipboardMonitor setDelay(long delay) { + this.delay = delay; + return this; + } + + /** + * 设置 监听事件处理 + * + * @param listener 监听事件处理 + * @return this + */ + public ClipboardMonitor addListener(ClipboardListener listener) { + this.listenerSet.add(listener); + return this; + } + + /** + * 去除指定监听 + * + * @param listener 监听 + * @return this + */ + public ClipboardMonitor removeListener(ClipboardListener listener) { + this.listenerSet.remove(listener); + return this; + } + + /** + * 清空监听 + * + * @return this + */ + public ClipboardMonitor clearListener() { + this.listenerSet.clear(); + return this; + } + + @Override + public void lostOwnership(Clipboard clipboard, Transferable contents) { + Transferable newContents; + try { + newContents = tryGetContent(clipboard); + } catch (InterruptedException e) { + // 中断后结束简体 + return; + } + + Transferable transferable = null; + for (ClipboardListener listener : listenerSet) { + try { + transferable = listener.onChange(clipboard, ObjectUtils.defaultIfNull(transferable, newContents)); + } catch (Throwable e) { + // 忽略事件处理异常,保证所有监听正常执行 + } + } + + if (isRunning) { + // 继续监听 + clipboard.setContents(ObjectUtils.defaultIfNull(transferable, ObjectUtils.defaultIfNull(newContents, contents)), this); + } + } + + @Override + public synchronized void run() { + if (false == isRunning) { + final Clipboard clipboard = this.clipboard; + clipboard.setContents(clipboard.getContents(null), this); + isRunning = true; + } + } + + /** + * 开始监听 + * + * @param sync 是否阻塞 + */ + public void listen(boolean sync) { + run(); + + if (sync) { + synchronized (this) { + try { + this.wait(); + } catch (InterruptedException e) { + // ignore + } + } + } + } + + /** + * 关闭(停止)监听 + */ + @Override + public void close() { + this.isRunning = false; + } + + // ------------------------------------------------------------------------------------------------------------------------ + // Private method start + /** + * 尝试获取剪贴板内容 + * + * @param clipboard 剪贴板 + * @return 剪贴板内容,{@code null} 表示未获取到 + * @throws InterruptedException 线程中断 + */ + private Transferable tryGetContent(Clipboard clipboard) throws InterruptedException { + Transferable newContents = null; + for (int i = 0; i < this.tryCount; i++) { + if (this.delay > 0 && i > 0) { + // 第一次获取不等待,只有从第二次获取时才开始等待 + // noinspection BusyWait + Thread.sleep(this.delay); + } + + try { + newContents = clipboard.getContents(null); + } catch (IllegalStateException e) { + // ignore + } + if (null != newContents) { + return newContents; + } + } + return null; + } + // ------------------------------------------------------------------------------------------------------------------------- + // Private method end +} diff --git a/src/main/java/com/luna/common/swing/ClipboardUtil.java b/src/main/java/com/luna/common/swing/ClipboardUtil.java new file mode 100755 index 000000000..f34ba59e6 --- /dev/null +++ b/src/main/java/com/luna/common/swing/ClipboardUtil.java @@ -0,0 +1,171 @@ +package com.luna.common.swing; + +import com.luna.common.exception.UtilException; + +import java.awt.*; +import java.awt.datatransfer.*; +import java.io.IOException; + +/** + * 系统剪贴板工具类 + * + * @author looly + * @since 3.2.0 + */ +public class ClipboardUtil { + + /** + * 获取系统剪贴板 + * + * @return {@link Clipboard} + */ + public static Clipboard getClipboard() { + return Toolkit.getDefaultToolkit().getSystemClipboard(); + } + + /** + * 设置内容到剪贴板 + * + * @param contents 内容 + */ + public static void set(Transferable contents) { + set(contents, null); + } + + /** + * 设置内容到剪贴板 + * + * @param contents 内容 + * @param owner 所有者 + */ + public static void set(Transferable contents, ClipboardOwner owner) { + getClipboard().setContents(contents, owner); + } + + /** + * 获取剪贴板内容 + * + * @param flavor 数据元信息,标识数据类型 + * @return 剪贴板内容,类型根据flavor不同而不同 + */ + public static Object get(DataFlavor flavor) { + return get(getClipboard().getContents(null), flavor); + } + + /** + * 获取剪贴板内容 + * + * @param content {@link Transferable} + * @param flavor 数据元信息,标识数据类型 + * @return 剪贴板内容,类型根据flavor不同而不同 + */ + public static Object get(Transferable content, DataFlavor flavor) { + if (null != content && content.isDataFlavorSupported(flavor)) { + try { + return content.getTransferData(flavor); + } catch (UnsupportedFlavorException | IOException e) { + throw new UtilException(e); + } + } + return null; + } + + /** + * 设置字符串文本到剪贴板 + * + * @param text 字符串文本 + */ + public static void setStr(String text) { + set(new StringSelection(text)); + } + + /** + * 从剪贴板获取文本 + * + * @return 文本 + */ + public static String getStr() { + return (String)get(DataFlavor.stringFlavor); + } + + /** + * 从剪贴板的{@link Transferable}获取文本 + * + * @param content {@link Transferable} + * @return 文本 + * @since 4.5.6 + */ + public static String getStr(Transferable content) { + return (String)get(content, DataFlavor.stringFlavor); + } + + /** + * 设置图片到剪贴板 + * + * @param image 图像 + */ + public static void setImage(Image image) { + set(new ImageSelection(image), null); + } + + /** + * 从剪贴板获取图片 + * + * @return 图片{@link Image} + */ + public static Image getImage() { + return (Image)get(DataFlavor.imageFlavor); + } + + /** + * 从剪贴板的{@link Transferable}获取图片 + * + * @param content {@link Transferable} + * @return 图片 + * @since 4.5.6 + */ + public static Image getImage(Transferable content) { + return (Image)get(content, DataFlavor.imageFlavor); + } + + /** + * 监听剪贴板修改事件 + * + * @param listener 监听处理接口 + * @since 4.5.6 + * @see ClipboardMonitor#listen(boolean) + */ + public static void listen(ClipboardListener listener) { + listen(listener, true); + } + + /** + * 监听剪贴板修改事件 + * + * @param listener 监听处理接口 + * @param sync 是否同步阻塞 + * @since 4.5.6 + * @see ClipboardMonitor#listen(boolean) + */ + public static void listen(ClipboardListener listener, boolean sync) { + listen(ClipboardMonitor.DEFAULT_TRY_COUNT, ClipboardMonitor.DEFAULT_DELAY, listener, sync); + } + + /** + * 监听剪贴板修改事件 + * + * @param tryCount 尝试获取剪贴板内容的次数 + * @param delay 响应延迟,当从第二次开始,延迟一定毫秒数等待剪贴板可以获取 + * @param listener 监听处理接口 + * @param sync 是否同步阻塞 + * @since 4.5.6 + * @see ClipboardMonitor#listen(boolean) + */ + public static void listen(int tryCount, long delay, ClipboardListener listener, boolean sync) { + ClipboardMonitor.INSTANCE// + .setTryCount(tryCount)// + .setDelay(delay)// + .addListener(listener)// + .listen(sync); + } +} diff --git a/src/main/java/com/luna/common/swing/DesktopUtil.java b/src/main/java/com/luna/common/swing/DesktopUtil.java new file mode 100755 index 000000000..dedc81d66 --- /dev/null +++ b/src/main/java/com/luna/common/swing/DesktopUtil.java @@ -0,0 +1,114 @@ +package com.luna.common.swing; + +import com.luna.common.exception.UtilException; +import com.luna.common.text.StringTools; + +import java.awt.*; +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; + +/** + * 桌面相关工具(平台相关)
+ * 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 数组元素类型 + * @param array 被检查的数组 + * @return 是否包含{@code null}元素 + * @since 3.0.7 + */ + @SuppressWarnings("unchecked") + public static boolean hasNull(T... array) { + if (isNotEmpty(array)) { + for (T element : array) { + if (isEmpty(element)) { + return true; + } + } + } + return array == null; + } + + /** + * 如果给定对象为{@code null}返回默认值 + * + *

+     * ObjectUtil.defaultIfNull(null, null)      = null
+     * ObjectUtil.defaultIfNull(null, "")        = ""
+     * ObjectUtil.defaultIfNull(null, "zz")      = "zz"
+     * ObjectUtil.defaultIfNull("abc", *)        = "abc"
+     * ObjectUtil.defaultIfNull(Boolean.TRUE, *) = Boolean.TRUE
+     * 
+ * + * @param 对象类型 + * @param object 被检查对象,可能为{@code null} + * @param defaultValue 被检查对象为{@code null}返回的默认值,可以为{@code null} + * @return 被检查对象为{@code null}返回默认值,否则返回原值 + * @since 3.0.7 + */ + public static T defaultIfNull(final T object, final T defaultValue) { + return isEmpty(object) ? defaultValue : object; + } + /** * Determine whether the given object is an array: * either an Object array or a primitive array. From 75531fbb7a1d0bf1e61dbf780fe4de4ed0b55104 Mon Sep 17 00:00:00 2001 From: chenzhangyue <15696756582@163.com> Date: Fri, 21 Jun 2024 16:17:23 +0800 Subject: [PATCH 2/4] :bookmark: add new tools --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 1365ea195..f162fe77e 100644 --- a/pom.xml +++ b/pom.xml @@ -12,9 +12,9 @@ https://github.com/lunasaw/luna-common - 1.8 - 1.8 - 1.8 + 17 + 17 + 17 UTF-8 UTF-8 From 99d55ff26f5d9f1f30210d89400de7fc130900fe Mon Sep 17 00:00:00 2001 From: chenzhangyue <15696756582@163.com> Date: Fri, 21 Jun 2024 16:36:25 +0800 Subject: [PATCH 3/4] :bookmark: add new tools --- .../com/luna/common/swing/DesktopUtil.java | 13 +++- .../java/com/luna/common/swing/RobotUtil.java | 70 ++++++++++++++----- .../com/luna/common/swing/ScreenUtil.java | 1 + 3 files changed, 64 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/luna/common/swing/DesktopUtil.java b/src/main/java/com/luna/common/swing/DesktopUtil.java index dedc81d66..5d84f1e0a 100755 --- a/src/main/java/com/luna/common/swing/DesktopUtil.java +++ b/src/main/java/com/luna/common/swing/DesktopUtil.java @@ -1,13 +1,14 @@ package com.luna.common.swing; -import com.luna.common.exception.UtilException; -import com.luna.common.text.StringTools; - import java.awt.*; import java.io.File; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.List; + +import com.luna.common.text.StringTools; /** * 桌面相关工具(平台相关)
@@ -18,6 +19,12 @@ */ public class DesktopUtil { + public static List getScreenDevices() { + GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); + GraphicsDevice[] gs = ge.getScreenDevices(); + return Arrays.asList(gs); + } + /** * 获得{@link Desktop} * diff --git a/src/main/java/com/luna/common/swing/RobotUtil.java b/src/main/java/com/luna/common/swing/RobotUtil.java index 3c38caf41..339b91674 100755 --- a/src/main/java/com/luna/common/swing/RobotUtil.java +++ b/src/main/java/com/luna/common/swing/RobotUtil.java @@ -7,6 +7,7 @@ import java.io.File; import com.luna.common.exception.UtilException; +import com.luna.common.file.FileTools; import com.luna.common.img.ImgUtil; /** @@ -17,13 +18,6 @@ */ 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; @@ -35,6 +29,10 @@ public static void main(String[] args) { } } + public static void main(String[] args) { + captureScreen(0, 0, 200, 300, FileTools.touch("/Users/weidian/Downloads/2.jpg")); + } + /** * 获取 Robot 单例实例 * @@ -45,6 +43,16 @@ public static Robot getRobot() { return ROBOT; } + /** + * 获取全局默认的延迟时间 + * + * @return 全局默认的延迟时间 + * @since 5.7.6 + */ + public static int getDelay() { + return delay; + } + /** * 设置默认的延迟时间
* 当按键执行完后的等待时间,也可以用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]; + } }