import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import javax.imageio.ImageIO; /** * 由单张 PNG 源图生成 jpackage 各平台所需的应用图标。 * *

jpackage 对各平台图标格式要求不同:Windows 用 {@code .ico}、macOS 用 * {@code .icns}、Linux 用 {@code .png}。本工具基于纯 JDK(ImageIO + AWT)实现转换, * 不依赖 ImageMagick/Pillow 等外部工具,可在 Windows/macOS/Linux 的构建机上直接运行。

* *

用法:{@code java IconGen.java },输出 * {@code icon.png}、{@code icon.ico}、{@code icon.icns}。

*/ public final class IconGen { /** ICO 中包含的图标尺寸 */ private static final int[] ICO_SIZES = {16, 24, 32, 48, 64, 128, 256}; /** 尺寸不大于该值时使用 BMP 条目,否则使用 PNG 条目(兼容性最佳) */ private static final int ICO_BMP_MAX_SIZE = 64; /** ICNS 条目:类型(4 字节)与其像素尺寸 */ private static final Object[][] ICNS_ENTRIES = { {"ic11", 32}, {"ic12", 64}, {"ic07", 128}, {"ic13", 256}, {"ic08", 256}, {"ic14", 512}, {"ic09", 512}, }; private IconGen() { } /** * 程序入口。 * * @param args 源 PNG 路径与输出目录 * @throws IOException 读写失败 */ public static void main(String[] args) throws IOException { if (args.length < 2) { System.err.println("用法: java IconGen.java "); System.exit(2); } Path source = Paths.get(args[0]); Path outputDir = Paths.get(args[1]); Files.createDirectories(outputDir); BufferedImage sourceImage = ImageIO.read(source.toFile()); if (sourceImage == null) { throw new IOException("无法读取源图标:" + source); } Files.copy(source, outputDir.resolve("icon.png"), StandardCopyOption.REPLACE_EXISTING); writeIco(sourceImage, outputDir.resolve("icon.ico")); writeIcns(sourceImage, outputDir.resolve("icon.icns")); System.out.println("已生成平台图标到 " + outputDir.toAbsolutePath()); } /** * 生成 Windows ICO 文件。 * * @param source 源图 * @param out 输出文件 * @throws IOException 写入失败 */ private static void writeIco(BufferedImage source, Path out) throws IOException { int count = ICO_SIZES.length; byte[][] images = new byte[count][]; for (int i = 0; i < count; i++) { BufferedImage scaled = resize(source, ICO_SIZES[i]); images[i] = ICO_SIZES[i] <= ICO_BMP_MAX_SIZE ? toBmpEntry(scaled) : toPng(scaled); } try (OutputStream os = Files.newOutputStream(out)) { writeShortLE(os, 0); writeShortLE(os, 1); writeShortLE(os, count); // 图标数据区起始偏移:6 字节文件头 + 每项 16 字节目录项 int offset = 6 + count * 16; for (int i = 0; i < count; i++) { int size = ICO_SIZES[i]; os.write(size >= 256 ? 0 : size); os.write(size >= 256 ? 0 : size); os.write(0); os.write(0); writeShortLE(os, 1); writeShortLE(os, 32); writeIntLE(os, images[i].length); writeIntLE(os, offset); offset += images[i].length; } for (byte[] image : images) { os.write(image); } } } /** * 将图像编码为 ICO 中的 BMP 条目:BITMAPINFOHEADER + 32 位 BGRA 像素 + AND 掩码。 * * @param image 图像 * @return BMP 条目字节 * @throws IOException 编码失败 */ private static byte[] toBmpEntry(BufferedImage image) throws IOException { int width = image.getWidth(); int height = image.getHeight(); ByteArrayOutputStream buffer = new ByteArrayOutputStream(); // BITMAPINFOHEADER(高度为两倍,包含 XOR 图与 AND 掩码) writeIntLE(buffer, 40); writeIntLE(buffer, width); writeIntLE(buffer, height * 2); writeShortLE(buffer, 1); writeShortLE(buffer, 32); writeIntLE(buffer, 0); writeIntLE(buffer, 0); writeIntLE(buffer, 0); writeIntLE(buffer, 0); writeIntLE(buffer, 0); writeIntLE(buffer, 0); // XOR 图:32 位 BGRA,自下而上 for (int y = height - 1; y >= 0; y--) { for (int x = 0; x < width; x++) { int argb = image.getRGB(x, y); buffer.write((argb) & 0xFF); buffer.write((argb >> 8) & 0xFF); buffer.write((argb >> 16) & 0xFF); buffer.write((argb >>> 24) & 0xFF); } } // AND 掩码:1 位/像素,行按 4 字节对齐,全 0(透明由 alpha 通道处理) int maskRowBytes = ((width + 31) / 32) * 4; byte[] maskRow = new byte[maskRowBytes]; for (int y = 0; y < height; y++) { buffer.write(maskRow); } return buffer.toByteArray(); } /** * 生成 macOS ICNS 文件(以 PNG 条目承载各尺寸)。 * * @param source 源图 * @param out 输出文件 * @throws IOException 写入失败 */ private static void writeIcns(BufferedImage source, Path out) throws IOException { ByteArrayOutputStream body = new ByteArrayOutputStream(); for (Object[] entry : ICNS_ENTRIES) { byte[] png = toPng(resize(source, (Integer) entry[1])); body.write(((String) entry[0]).getBytes("US-ASCII")); writeIntBE(body, png.length + 8); body.write(png); } byte[] bodyBytes = body.toByteArray(); try (OutputStream os = Files.newOutputStream(out)) { os.write("icns".getBytes("US-ASCII")); writeIntBE(os, bodyBytes.length + 8); os.write(bodyBytes); } } /** * 缩放图像。 * * @param source 源图 * @param size 目标边长 * @return 缩放后的 ARGB 图像 */ private static BufferedImage resize(BufferedImage source, int size) { BufferedImage target = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB); Graphics2D graphics = target.createGraphics(); try { graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); graphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); graphics.drawImage(source, 0, 0, size, size, null); } finally { graphics.dispose(); } return target; } /** * 将图像编码为 PNG 字节。 * * @param image 图像 * @return PNG 字节 * @throws IOException 编码失败 */ private static byte[] toPng(BufferedImage image) throws IOException { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); ImageIO.write(image, "png", buffer); return buffer.toByteArray(); } /** * 写出小端 2 字节整数。 * * @param os 输出流 * @param value 值 * @throws IOException 写入失败 */ private static void writeShortLE(OutputStream os, int value) throws IOException { os.write(value & 0xFF); os.write((value >> 8) & 0xFF); } /** * 写出小端 4 字节整数。 * * @param os 输出流 * @param value 值 * @throws IOException 写入失败 */ private static void writeIntLE(OutputStream os, int value) throws IOException { os.write(value & 0xFF); os.write((value >> 8) & 0xFF); os.write((value >> 16) & 0xFF); os.write((value >>> 24) & 0xFF); } /** * 写出大端 4 字节整数。 * * @param os 输出流 * @param value 值 * @throws IOException 写入失败 */ private static void writeIntBE(OutputStream os, int value) throws IOException { os.write((value >>> 24) & 0xFF); os.write((value >> 16) & 0xFF); os.write((value >> 8) & 0xFF); os.write(value & 0xFF); } }