diff --git a/pom.xml b/pom.xml index a75bb37..f2bcbf3 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ top.gtb520.java.gnu gnu_java_utils_library - 1.5-SNAPSHOT + 1.6-SNAPSHOT 25 diff --git a/src/main/java/top/gtb520/java/gnu/java/utils/library/main.java b/src/main/java/top/gtb520/java/gnu/java/utils/library/main.java index 187bea1..b54840d 100644 --- a/src/main/java/top/gtb520/java/gnu/java/utils/library/main.java +++ b/src/main/java/top/gtb520/java/gnu/java/utils/library/main.java @@ -1,6 +1,7 @@ package top.gtb520.java.gnu.java.utils.library; import top.gtb520.java.gnu.java.utils.library.utils.print.ColorNameEnum; +import top.gtb520.java.gnu.java.utils.library.utils.compress.CompressPNG.PNGThreshold; import java.io.File; import java.nio.file.Path; @@ -31,6 +32,11 @@ public interface main { static void UnZip(String sourceFilePanth, String destFilePanth) throws Exception { top.gtb520.java.gnu.java.utils.library.utils.compress.compress.UnZip(sourceFilePanth, destFilePanth); } + + // 压缩png图片 + static File CompressPNG(File pngFile, PNGThreshold threshold) { + return top.gtb520.java.gnu.java.utils.library.utils.compress.CompressPNG.PNG(pngFile, threshold); + } } // 终端打印接口与方法 diff --git a/src/main/java/top/gtb520/java/gnu/java/utils/library/utils/compress/CompressPNG.java b/src/main/java/top/gtb520/java/gnu/java/utils/library/utils/compress/CompressPNG.java index 9ff52c8..3d1a9dd 100644 --- a/src/main/java/top/gtb520/java/gnu/java/utils/library/utils/compress/CompressPNG.java +++ b/src/main/java/top/gtb520/java/gnu/java/utils/library/utils/compress/CompressPNG.java @@ -1,4 +1,82 @@ package top.gtb520.java.gnu.java.utils.library.utils.compress; +import javax.imageio.IIOImage; +import javax.imageio.ImageIO; +import javax.imageio.ImageWriteParam; +import javax.imageio.ImageWriter; +import javax.imageio.stream.ImageOutputStream; +import java.io.File; +import java.awt.image.BufferedImage; +import java.util.Iterator; + public class CompressPNG { + /*** + * 压缩PNG图片 + * 传入未压缩的PNG图片文件,和压缩阈值,返回压缩后的PNG图片文件 + * 5个等级的压缩阈值,从低到高,THRESHOLD_1 最低,THRESHOLD_5 最高 + * + * @param pngFile 未压缩的PNG图片文件 + * @param threshold 压缩阈值 + * @return 压缩后的PNG图片文件 + */ + public static File PNG(File pngFile, PNGThreshold threshold) { + if (!pngFile.exists()) { + System.out.println("文件不存在"); + return null; + } + if (pngFile.getName().endsWith(".png") || pngFile.getName().endsWith(".PNG")) { + try { + BufferedImage image = ImageIO.read(pngFile); + if (image == null) { + System.out.println("无法读取PNG文件"); + return null; + } + + Iterator writers = ImageIO.getImageWritersByFormatName("png"); + if (!writers.hasNext()) { + System.out.println("未找到PNG写入器"); + return null; + } + ImageWriter writer = writers.next(); + + try (ImageOutputStream ios = ImageIO.createImageOutputStream(pngFile)) { + writer.setOutput(ios); + + ImageWriteParam param = writer.getDefaultWriteParam(); + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + param.setCompressionQuality(getCompressionQuality(threshold)); + + writer.write(null, new IIOImage(image, null, null), param); + } finally { + writer.dispose(); + } + + return pngFile; + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } else { + System.out.println("文件不是PNG格式,无法压缩"); + } + return null; + } + + private static float getCompressionQuality(PNGThreshold threshold) { + return switch (threshold) { + case THRESHOLD_1 -> 0.7f; + case THRESHOLD_2 -> 0.5f; + case THRESHOLD_3 -> 0.3f; + case THRESHOLD_4 -> 0.1f; + case THRESHOLD_5 -> 0.0f; + }; + } + + public enum PNGThreshold { + THRESHOLD_1, + THRESHOLD_2, + THRESHOLD_3, + THRESHOLD_4, + THRESHOLD_5; + } } diff --git a/src/test/java/top/gtb520/java/gnu/java/utils/library/utils/compress/CompressPNGTest.java b/src/test/java/top/gtb520/java/gnu/java/utils/library/utils/compress/CompressPNGTest.java new file mode 100644 index 0000000..11ff033 --- /dev/null +++ b/src/test/java/top/gtb520/java/gnu/java/utils/library/utils/compress/CompressPNGTest.java @@ -0,0 +1,205 @@ +package top.gtb520.java.gnu.java.utils.library.utils.compress; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; + +public class CompressPNGTest { + + static int passed = 0; + static int failed = 0; + + public static void main(String[] args) { + System.out.println("========== CompressPNG 测试开始 =========="); + + testCompressWithEachThreshold(); + testNonPNGFile(); + testNonExistentFile(); + testCompressedFileIsValidPNG(); + testCompressedFileSmallerOrEqual(); + + System.out.println("========================================"); + System.out.println("测试完成: 通过 " + passed + ", 失败 " + failed); + if (failed > 0) { + System.exit(1); + } + } + + /** + * 测试每个压缩阈值均能正常压缩并返回原文件 + */ + static void testCompressWithEachThreshold() { + String testName = "testCompressWithEachThreshold"; + for (CompressPNG.PNGThreshold threshold : CompressPNG.PNGThreshold.values()) { + File pngFile = null; + try { + pngFile = createTestPNG("test_threshold_" + threshold.name()); + long originalSize = pngFile.length(); + + File result = CompressPNG.PNG(pngFile, threshold); + + boolean returnedSameFile = result != null && result.equals(pngFile); + assertCondition(testName + " [" + threshold.name() + "] - 应返回原文件引用", returnedSameFile); + + boolean fileExists = result != null && result.exists() && result.length() > 0; + assertCondition(testName + " [" + threshold.name() + "] - 压缩后文件应存在且非空", fileExists); + + System.out.println(" [" + threshold.name() + "] 原始: " + originalSize + " bytes, 压缩后: " + (result != null ? result.length() : "N/A") + " bytes"); + } catch (Exception e) { + assertCondition(testName + " [" + threshold.name() + "] - 不应抛出异常: " + e.getMessage(), false); + } finally { + deleteTestFile(pngFile); + } + } + } + + /** + * 测试传入非PNG文件应返回null + */ + static void testNonPNGFile() { + String testName = "testNonPNGFile"; + File txtFile = null; + try { + txtFile = File.createTempFile("test_not_png", ".txt"); + java.nio.file.Files.writeString(txtFile.toPath(), "this is not a png"); + + File result = CompressPNG.PNG(txtFile, CompressPNG.PNGThreshold.THRESHOLD_3); + assertCondition(testName + " - 非PNG文件应返回null", result == null); + } catch (Exception e) { + assertCondition(testName + " - 不应抛出异常: " + e.getMessage(), false); + } finally { + deleteTestFile(txtFile); + } + } + + /** + * 测试传入不存在的文件应返回null + */ + static void testNonExistentFile() { + String testName = "testNonExistentFile"; + try { + File nonExistent = new File(System.getProperty("java.io.tmpdir"), "non_existent_file.png"); + File result = CompressPNG.PNG(nonExistent, CompressPNG.PNGThreshold.THRESHOLD_3); + assertCondition(testName + " - 不存在的文件应返回null", result == null); + } catch (Exception e) { + assertCondition(testName + " - 不应抛出异常: " + e.getMessage(), false); + } + } + + /** + * 测试压缩后的文件仍然是有效的PNG图片 + */ + static void testCompressedFileIsValidPNG() { + String testName = "testCompressedFileIsValidPNG"; + File pngFile = null; + try { + pngFile = createTestPNG("test_valid_png"); + CompressPNG.PNG(pngFile, CompressPNG.PNGThreshold.THRESHOLD_3); + + BufferedImage compressed = ImageIO.read(pngFile); + boolean isValidPNG = compressed != null; + assertCondition(testName + " - 压缩后的文件应是有效的PNG图片", isValidPNG); + + if (isValidPNG) { + assertCondition(testName + " - 压缩后图片宽度应一致", compressed.getWidth() == 100); + assertCondition(testName + " - 压缩后图片高度应一致", compressed.getHeight() == 100); + } + } catch (Exception e) { + assertCondition(testName + " - 不应抛出异常: " + e.getMessage(), false); + } finally { + deleteTestFile(pngFile); + } + } + + /** + * 测试压缩后文件大小不超过原始文件 + */ + static void testCompressedFileSmallerOrEqual() { + String testName = "testCompressedFileSmallerOrEqual"; + File pngFile = null; + try { + // 创建一个较大的、有压缩空间的测试图片(纯色+渐变) + pngFile = createLargeTestPNG("test_compression_ratio", 500, 500); + long originalSize = pngFile.length(); + + File result = CompressPNG.PNG(pngFile, CompressPNG.PNGThreshold.THRESHOLD_5); + + if (result != null) { + long compressedSize = result.length(); + assertCondition(testName + " - 压缩后文件大小(" + compressedSize + ")应不超过原始大小(" + originalSize + ")", + compressedSize <= originalSize); + System.out.println(" 原始: " + originalSize + " bytes, THRESHOLD_5压缩后: " + compressedSize + " bytes"); + } else { + assertCondition(testName + " - 压缩不应返回null", false); + } + } catch (Exception e) { + assertCondition(testName + " - 不应抛出异常: " + e.getMessage(), false); + } finally { + deleteTestFile(pngFile); + } + } + + // ========== 辅助方法 ========== + + /** + * 创建一个100x100的测试PNG图片 + */ + static File createTestPNG(String name) throws IOException { + File file = File.createTempFile(name, ".png"); + BufferedImage image = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2d = image.createGraphics(); + // 绘制渐变图案,使图片有一定的压缩空间 + for (int y = 0; y < 100; y++) { + for (int x = 0; x < 100; x++) { + image.setRGB(x, y, new Color(x * 2, y * 2, (x + y)).getRGB()); + } + } + g2d.dispose(); + ImageIO.write(image, "png", file); + return file; + } + + /** + * 创建一个指定尺寸的测试PNG图片(包含更多颜色变化以增加压缩效果差异) + */ + static File createLargeTestPNG(String name, int width, int height) throws IOException { + File file = File.createTempFile(name, ".png"); + BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2d = image.createGraphics(); + // 绘制大面积纯色区域(压缩率高) + g2d.setColor(Color.WHITE); + g2d.fillRect(0, 0, width, height); + g2d.setColor(Color.RED); + g2d.fillRect(0, 0, width / 2, height / 2); + g2d.setColor(Color.BLUE); + g2d.fillRect(width / 2, 0, width / 2, height / 2); + g2d.setColor(Color.GREEN); + g2d.fillRect(0, height / 2, width / 2, height / 2); + g2d.setColor(Color.YELLOW); + g2d.fillRect(width / 2, height / 2, width / 2, height / 2); + g2d.dispose(); + ImageIO.write(image, "png", file); + return file; + } + + /** + * 安全删除测试文件 + */ + static void deleteTestFile(File file) { + if (file != null && file.exists()) { + file.delete(); + } + } + + static void assertCondition(String message, boolean condition) { + if (condition) { + System.out.println("[PASS] " + message); + passed++; + } else { + System.out.println("[FAIL] " + message); + failed++; + } + } +}