Java使用PNG图片隐写文件的示例代码

 更新时间:2026年06月10日 09:31:45   作者:西凉的悲伤  
这篇文章主要介绍了LSB(最低有效位)图片隐写原理,及其在PNG图片中的应用,文章解释了隐写容量计算公式,并通过实例展示了如何计算不同像素图片的隐写容量,此外,文章还对比了PNG与JPG在隐写上的区别,并提供了Java实现示例,需要的朋友可以参考下

一、图片隐写原理(LSB)

最经典的是 LSB(最低有效位)

图片像素:

RGB:
11111110

修改最后1位:

11111111

肉眼几乎看不出来,于是,每个颜色通道藏1bit

一个像素:

R + G + B = 3bit

即 一个像素 可隐写 3bit 数据,所以隐写容量取决于像素数量

二、 可隐写文件容量大小计算

公式:可隐写的文件大小 (单位bytes) = (width × height × 3) / 8

举例:

像素:1920×1080

可隐写文件容量:(1920×1080 × 3) / 8 ≈ 777,600 bytes ≈ 759KB

像素:2560 ×1440

可隐写文件容量:(2560 ×1440 × 3) / 8 ≈ 1,382,400 bytes ≈ 1.32MB

4K 图片像素:3840 × 2160

可隐写文件容量:(3840 × 2160 × 3) / 8 ≈ 3.1MB

三、文件隐写和还原步骤

文件隐写:

文件 -> AES加密 -> 文件隐写入png图片

png图片还原文件:

文件隐写入png图片 -> AES解密  -> 文件

四、为什么 PNG 可以隐写文件,而 JPG 不行?

因为 PNG 是无损压缩,PNG 保存图片时像素值不会变化,通过修改RGB最低位,100%保留,所以 LSB 隐写非常适合 PNG。

PNG vs JPG 隐写区别:

对比PNGJPG
压缩方式无损有损
能否直接LSB可以不可以
数据稳定性极高极低
图片体积较大较小
抗平台压缩
隐写难度简单很复杂
Java实现很容易很难

五、隐写工具类

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

import javax.crypto.*;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.security.spec.KeySpec;

public class FileHidenToPicUtil {

    private static final String AES_ALGORITHM = "AES/CBC/PKCS5Padding";

    private static final int KEY_SIZE = 256;

    private static final int ITERATIONS = 65536;

    private static final String password = "2026@InternalFILE#Transfer";

    /**
     * 文件隐写入png图片文件
     *
     * @param inputFile      待隐写入的文件路径,如:C:\\Users\\haitang\\Desktop\\xa.pdf"
     * @param coverPngImage  png文件路径,如:C:\Users\haitang\Desktop\Snipaste.png
     * @param outputPngImage 最终生成的 含有隐写入文件的png路径,如:如:C:\Users\haitang\Desktop\new.png
     */
    public static void fileHidenToPic(String inputFile, String coverPngImage, String outputPngImage) throws Exception {
        //读取文件
        byte[] fileBytes = Files.readAllBytes(Paths.get(inputFile));
        // gzip压缩
        byte[] compressed = gzip(fileBytes);
        // AES
        byte[] encrypted = encrypt(compressed, password);
        // 读取图片
        BufferedImage image = ImageIO.read(new File(coverPngImage));
        // 写入数据
        BufferedImage output = embedData(image, encrypted);
        // 输出PNG
        ImageIO.write(output, "png", new File(outputPngImage));
        System.out.println("隐写完成");
    }

    /**
     * 隐写的png文件还原为真实文件
     *
     * @param inputPNGImage 待还原的隐写png文件路径,如:C:\\Users\\haitang\\Desktop\\new.png"
     * @param outputFile    png文件路径,如:C:\Users\haitang\Desktop\recover.pdf
     */
    public static void reCoverFile(String inputPNGImage, String outputFile) throws Exception {
        BufferedImage image = ImageIO.read(new File(inputPNGImage));
        // 提取
        byte[] encrypted = extractData(image);
        // AES解密
        byte[] compressed = decrypt(encrypted, password);
        // gunzip
        byte[] fileBytes = gunzip(compressed);
        Files.write(Paths.get(outputFile), fileBytes);
        System.out.println("恢复成功");
    }


    //AES加密
    private static byte[] encrypt(byte[] data, String password) throws Exception {
        // 生成salt
        byte[] salt = new byte[16];
        SecureRandom random = new SecureRandom();
        random.nextBytes(salt);

        // 派生密钥
        SecretKey key = deriveKey(password, salt);
        // 生成IV
        byte[] iv = new byte[16];

        random.nextBytes(iv);
        Cipher cipher = Cipher.getInstance(AES_ALGORITHM);

        cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));

        byte[] encrypted = cipher.doFinal(data);

        // 保存 salt + iv + data
        ByteArrayOutputStream bos = new ByteArrayOutputStream();

        bos.write(salt);
        bos.write(iv);
        bos.write(encrypted);
        return bos.toByteArray();
    }

    //AES解密
    private static byte[] decrypt(byte[] encryptedData, String password) throws Exception {
        ByteArrayInputStream bis = new ByteArrayInputStream(encryptedData);
        // 读取salt
        byte[] salt = new byte[16];
        bis.read(salt);
        // 读取iv
        byte[] iv = new byte[16];
        bis.read(iv);

        // 剩余是密文
        byte[] encrypted = new byte[bis.available()];
        bis.read(encrypted);

        SecretKey key = deriveKey(password, salt);

        Cipher cipher = Cipher.getInstance(AES_ALGORITHM);

        cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));

        return cipher.doFinal(encrypted);
    }

    //PBKDF2 密钥派生
    private static SecretKey deriveKey(String password, byte[] salt) throws Exception {
        SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");

        KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, ITERATIONS, KEY_SIZE);

        byte[] keyBytes = factory.generateSecret(spec).getEncoded();

        return new SecretKeySpec(keyBytes, "AES");
    }

    /**
     * 数据写入LSB
     */
    private static BufferedImage embedData(BufferedImage image, byte[] data) throws Exception {
        int width = image.getWidth();
        int height = image.getHeight();

        BufferedImage output = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);

        // 复制原图
        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                output.setRGB(x, y, image.getRGB(x, y));
            }
        }

        // 数据长度(4字节)
        byte[] lengthBytes = intToBytes(data.length);

        // 合并长度 + 数据
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        bos.write(lengthBytes);
        bos.write(data);

        byte[] allData = bos.toByteArray();

        //检查图片容量是否足够隐写文件
        int capacity = width * height * 3 / 8;
        if (allData.length > capacity) {
            double capacityMB = capacity / 1024.0 / 1024.0;
            double requiredMB = allData.length / 1024.0 / 1024.0;
            throw new RuntimeException(
                    "图片隐写容量不足!\n" +
                            "当前图片最大隐写的文件大小: " + String.format("%.2f", capacityMB) + " MB\n" +
                            "当前文件压缩后大小: " + String.format("%.2f", requiredMB) + " MB\n"+
                            "请更换分辨率更大的png文件,或者更换更小的要隐写的文件。"
            );
        }

        int dataIndex = 0;
        int bitIndex = 0;

        outer:
        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                int rgb = output.getRGB(x, y);
                int r = (rgb >> 16) & 0xff;
                int g = (rgb >> 8) & 0xff;
                int b = rgb & 0xff;

                // R
                if (dataIndex < allData.length) {
                    r = setLSB(r, getBit(allData[dataIndex], bitIndex));
                    bitIndex++;
                    if (bitIndex == 8) {
                        bitIndex = 0;
                        dataIndex++;
                    }
                }

                // G
                if (dataIndex < allData.length) {
                    g = setLSB(g, getBit(allData[dataIndex], bitIndex));
                    bitIndex++;
                    if (bitIndex == 8) {
                        bitIndex = 0;
                        dataIndex++;
                    }
                }

                // B
                if (dataIndex < allData.length) {
                    b = setLSB(b, getBit(allData[dataIndex], bitIndex));
                    bitIndex++;
                    if (bitIndex == 8) {
                        bitIndex = 0;
                        dataIndex++;
                    }
                }

                int newRgb = (r << 16) | (g << 8) | b;
                output.setRGB(x, y, newRgb);

                if (dataIndex >= allData.length) {
                    break outer;
                }
            }
        }

        return output;
    }

    /**
     * 设置最低位
     */
    private static int setLSB(int value, int bit) {
        return (value & 0xFE) | bit;
    }

    /**
     * 获取bit
     */
    private static int getBit(byte b, int index) {
        return (b >> (7 - index)) & 1;
    }

    /**
     * int转byte[]
     */
    private static byte[] intToBytes(int value) {
        return new byte[]{
                (byte) (value >> 24),
                (byte) (value >> 16),
                (byte) (value >> 8),
                (byte) value
        };
    }

    /**
     * gzip
     */
    private static byte[] gzip(byte[] data) throws Exception {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        GZIPOutputStream gzip = new GZIPOutputStream(bos);
        gzip.write(data);
        gzip.close();
        return bos.toByteArray();
    }

    /**
     * 提取LSB数据
     */
    private static byte[] extractData(BufferedImage image) throws Exception {
        int width = image.getWidth();
        int height = image.getHeight();
        ByteArrayOutputStream bos = new ByteArrayOutputStream();

        int currentByte = 0;
        int bitCount = 0;
        int dataLength = -1;
        int bytesRead = 0;

        outer:
        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                int rgb = image.getRGB(x, y);
                int[] colors = {(rgb >> 16) & 0xff, (rgb >> 8) & 0xff, rgb & 0xff};
                for (int color : colors) {
                    int bit = color & 1;
                    currentByte = (currentByte << 1) | bit;
                    bitCount++;
                    if (bitCount == 8) {
                        bos.write(currentByte);
                        bytesRead++;
                        // 前4字节是长度
                        if (bytesRead == 4) {
                            byte[] lenBytes = bos.toByteArray();
                            dataLength = bytesToInt(lenBytes);
                            bos.reset();
                        }

                        // 数据读取完成
                        if (dataLength != -1 && bos.size() == dataLength) {
                            break outer;
                        }
                        currentByte = 0;
                        bitCount = 0;
                    }
                }
            }
        }
        return bos.toByteArray();
    }

    /**
     * byte[]转int
     */
    private static int bytesToInt(byte[] bytes) {
        return ((bytes[0] & 0xff) << 24) |
                ((bytes[1] & 0xff) << 16) |
                ((bytes[2] & 0xff) << 8) |
                (bytes[3] & 0xff);
    }

    /**
     * gunzip
     */
    private static byte[] gunzip(byte[] data) throws Exception {
        ByteArrayInputStream bis = new ByteArrayInputStream(data);
        GZIPInputStream gzip = new GZIPInputStream(bis);
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int len;
        while ((len = gzip.read(buffer)) > 0) {
            bos.write(buffer, 0, len);
        }
        return bos.toByteArray();
    }
}

六、测试

可以先下载该 png 图片,图片分辨率 5120 x 2880 大约可以隐写 5MB 的文件。
链接:https://i-blog.csdnimg.cn/direct/4b54f5f678bb404f8f92007792e46592.png

或者去 https://4kwallpapers.com/uncompressed-png-wallpapers/ 下载其他高清 png 图片

1.文件隐写入png文件

        String inputFile = "C:\\Users\\haitang\\Desktop\\aaa.pdf";
        String coverPngImage = "C:\\Users\\haitang\\Desktop\\Snipaste.png";
        String outputPngImage = "C:\\Users\\haitang\\Desktop\\result.png";
        FileHidenToPicUtil.fileHidenToPic(inputFile, coverPngImage, outputPngImage);

2.png文件还原文件

        String inputPngImage="C:\\Users\\haitang\\Desktop\\result.png";
        String outputFile="C:\\Users\\haitang\\Desktop\\aaaNew.pdf";
        FileHidenToPicUtil.reCoverFile(inputPngImage,outputFile);

以上就是Java使用PNG图片隐写文件的示例代码的详细内容,更多关于Java PNG图片隐写文件的资料请关注脚本之家其它相关文章!

相关文章

  • java中利用List的subList方法实现对List分页(简单易学)

    java中利用List的subList方法实现对List分页(简单易学)

    本篇文章主要介绍了java中list数据拆分为sublist实现页面分页的简单代码,具有一定的参考价值,有需要的可以了解一下。
    2016-11-11
  • Java BeanPostProcessor与BeanFactoryPostProcessor基础使用讲解

    Java BeanPostProcessor与BeanFactoryPostProcessor基础使用讲解

    这篇文章主要介绍了Java BeanPostProcessor与BeanFactoryPostProcessor基础使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习吧
    2022-11-11
  • Java 9中List.of()的使用示例及注意事项

    Java 9中List.of()的使用示例及注意事项

    Java 9引入了一个新的静态工厂方法List.of(),用于创建不可变的列表对象,这篇文章主要介绍了Java 9中List.of()的使用示例及注意事项的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2025-03-03
  • MyBatis-Plus雪花算法实现源码解读

    MyBatis-Plus雪花算法实现源码解读

    雪花算法是一种用于生成唯一标识符(ID)的分布式算法,雪花算法的设计目标是在分布式系统中生成全局唯一的ID,同时保证ID的有序性和趋势递增,这篇文章主要介绍了MyBatis-Plus雪花算法实现源码解析,需要的朋友可以参考下
    2023-12-12
  • java字符转码的三种方法总结及实例

    java字符转码的三种方法总结及实例

    这篇文章主要介绍了 java字符转码的三种方法总结及实例的相关资料,需要的朋友可以参考下
    2017-03-03
  • 浅谈java中String与StringBuffer的不同

    浅谈java中String与StringBuffer的不同

    String在栈中,StringBuffer在堆中!所以String是不可变的,数据是共享的。StringBuffer都是独占的,是可变的(因为每次都是创建新的对象!)
    2015-11-11
  • java实现Object和Map之间的转换3种方式

    java实现Object和Map之间的转换3种方式

    本篇文章主要介绍了java实现Object和Map之间的转换3种方式,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-06-06
  • Java线程池使用AbortPolicy策略

    Java线程池使用AbortPolicy策略

    这篇文章主要介绍了 Java线程池使用AbortPolicy策略,文章围绕主题展开详细的内容介绍,具有一定的参考价值,感兴趣的小伙伴可以参考一下
    2022-06-06
  • swagger2隐藏在API文档显示某些参数的操作

    swagger2隐藏在API文档显示某些参数的操作

    这篇文章主要介绍了swagger2隐藏在API文档显示某些参数的操作,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-06-06
  • JavaSE实现电影院系统

    JavaSE实现电影院系统

    这篇文章主要为大家详细介绍了JavaSE实现电影院系统,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-08-08

最新评论