Python 位操作实现图片隐写

适用RGBA模式(带透明度信息)的PNG文件

使用时可以自己把编码和解码功能拆分一下

"""Encode png image via command-line.

Usage:
    imageEncoding (-e|encode) <originImage> [<text>] [<encodedImage>]
    imageEncoding (-d|decode) <encodedImage>

Options:
    -h,--help   显示帮助菜单
    -e          加密
    -d          解密

Example:
    imageEncoding -e coffee.png hello textOrFileToEncode encodedImage.png
    imageEncoding -d encodedImage.png
"""

from PIL import Image
from docopt import docopt

# 取得一个 PIL 图像并且更改所有值为偶数(使最低有效位为 0)
# 接受一个 PIL图像对象作为输入,通过一种特殊的位操作技巧,返回一个新的图像对象,其中每个像素的 RGBA 值都被转换为最接近的偶数
def RGBAmakeImageEven(image):
    # 将图像的像素数据转换为列表,其中每个元素是一个包含 RGBA 值的元组
    pixels = list(image.getdata())       
    # 创建一个新的列表,其中每个像素的 RGBA 值都被转换为最接近的偶数  
    # 右移一位(相当于除以 2),然后左移一位(相当于乘以 2) 
    # 如果原始值是奇数,右移一位后最低位会变成 0,左移一位后仍然保持为偶数  
    # 如果原始值是偶数,则这个操作不会改变其值  
    evenPixels = [(r>>1<<1,g>>1<<1,b>>1<<1,t>>1<<1) for [r,g,b,t] in pixels] 
    # 创建一个相同大小的图片副本
    evenImage = Image.new(image.mode, image.size)  
    # 将处理后的像素数据放回新图像对象中
    evenImage.putdata(evenPixels)
    return evenImage

# 内置函数 bin() 的替代,返回固定长度的二进制字符串
# 将一个整数转换为一个固定长度为8的二进制字符串表示。如果整数的二进制表示长度小于8,则在左侧用'0'来填充
def constLenBin(int):
    binary = "0"*(8-(len(bin(int))-2))+bin(int).replace('0b','')  # 去掉 bin() 返回的二进制字符串中的 '0b',并在左边补足 '0' 直到字符串长度为 8
    return binary

# 将字符串编码到图片中
# 最低有效位(Least Significant Bit, LSB)
def RGBAencodeDataInImage(image, data):
    # 获得最低有效位为0(每个像素的 RGBA 值都被转换为最接近的偶数)的图片副本
    evenImage = RGBAmakeImageEven(image)  
    # 将要编码的字符串'data'转换为字节数组,然后进一步转换为二进制字符串  
    # constLenBin 函数确保每个字节被转换为固定长度的8位二进制字符串  
    binary = ''.join(map(constLenBin,bytearray(data, 'utf-8'))) 
    # 检查要编码的二进制字符串是否超过了图像像素所能容纳的位数  
    # 如果二进制字符串太长,则抛出异常 
    if len(binary) > len(image.getdata()) * 4: 
        raise Exception("Error: Can't encode more than " + len(evenImage.getdata()) * 4 + " bits in this image. ")
    # 将 binary 中的二进制字符串信息编码进像素   
    # 将像素的 RGBA 值与二进制数据的相应位相加(利用上一步实现的最低有效位为0的特点)                 
    encodedPixels = [(r+int(binary[index*4+0]),g+int(binary[index*4+1]),b+int(binary[index*4+2]),t+int(binary[index*4+3])) if index*4 < len(binary) else (r,g,b,t) for index,(r,g,b,t) in enumerate(list(evenImage.getdata()))] 
    encodedImage = Image.new(evenImage.mode, evenImage.size)  # 创建新图片以存放编码后的像素
    encodedImage.putdata(encodedPixels)  # 添加编码后的数据
    return encodedImage

# 从二进制字符串转为 UTF-8 字符串
def binaryToString(binary):
    # 初始化索引为0,用于遍历二进制字符串  
    index = 0  
    # 初始化一个空列表,用于存储解码后的字符  
    string = []
    # 参数x是当前处理的二进制字符串,i是递归深度(字符的字节数)  
    rec = lambda x, i: x[2:8] + (rec(x[8:], i-1) if i > 1 else '') if x else ''
    fun = lambda x, i: x[i+1:8] + rec(x[8:], i-1)
    while index + 1 < len(binary):
        chartype = binary[index:].index('0') 
        length = chartype*8 if chartype else 8
        # fun函数从二进制字符串中提取字符,并转换为整数  
        # chr函数将整数转换为对应的字符,并添加到string列表中  
        string.append(chr(int(fun(binary[index:index+length],chartype),2)))
        # 更新索引,以便处理下一个字符
        index += length
    return ''.join(string)

# 解码隐藏数据
def RGBAdecodeImage(image):
    pixels = list(image.getdata())  # 获得像素列表
    binary = ''.join([str(int(r>>1<<1!=r))+str(int(g>>1<<1!=g))+str(int(b>>1<<1!=b))+str(int(t>>1<<1!=t)) for (r,g,b,t) in pixels]) # 提取图片中所有最低有效位中的数据
    # 找到数据截止处的索引
    locationDoubleNull = binary.find('0000000000000000')
    endIndex = locationDoubleNull+(8-(locationDoubleNull % 8)) if locationDoubleNull%8 != 0 else locationDoubleNull
    data = binaryToString(binary[0:endIndex])
    return data

def RGBdecodeImage(image):
    # 使用 image.getdata() 方法从图片对象中获取所有的像素数据,并将其转换为一个列表
    # 每个像素都是一个包含四个值的元组,分别代表红色、绿色、蓝色和透明度(alpha)的值
    pixels = list(image.getdata()) 
    # 对于图片中的每个像素,提取红色、绿色、蓝色和透明度值的最低有效位(LSB)
    # 通过位运算 r>>1<<1!=r 来完成:
    # 检查最低有效位是否为1。如果是1,则表达式的结果为True,转换为整数为1;如果是0,则结果为False,转换为整数为0
    binary = ''.join([str(int(r>>1<<1!=r))+str(int(g>>1<<1!=g))+str(int(b>>1<<1!=b)) for (r,g,b) in pixels]) # 提取图片中所有最低有效位中的数据
    # 结束标记:两个连续的零字节(16个连续的零位)
    locationDoubleNull = binary.find('0000000000000000') # 找到数据截止处的索引
    endIndex = locationDoubleNull+(8-(locationDoubleNull % 8)) if locationDoubleNull%8 != 0 else locationDoubleNull
    data = binaryToString(binary[0:endIndex])
    return data

#这个函数的作用主要是判断传入的是str还是可加密文件
def isTextFile(path):
    if path.endswith(".txt"):
        return True
    elif path.endswith(".m"):
        return True
    elif path.endswith(".h"):
        return True
    elif path.endswith(".c"):
        return True
    elif path.endswith(".py"):
        return True
    else:
        return False

if __name__ == '__main__':
    """command-line interface"""
    arguments = docopt(__doc__)

    # 参数为-e则进行加密
    if arguments['-e'] or arguments['encode']:
        if arguments['<text>'] is None:
            arguments['<text>'] = "待加密的文本"
        if arguments['<encodedImage>'] is None:
            arguments['<encodedImage>'] = "encodedImage.png"

        if isTextFile(arguments['<text>']):
            with open(arguments['<text>'], 'rt') as f:
                arguments['<text>'] = f.read()

        print("载体图片:")
        print(arguments['<originImage>']+"\n")
        print("待加密密文:")
        print(arguments['<text>']+"\n")
        print("加密后图片:")
        print(arguments['<encodedImage>']+"\n")
        print("加密中……\n")
        im = Image.open(arguments['<originImage>'])
        if im.mode == 'RGBA':
            RGBAencodeDataInImage(im, arguments['<text>']).save(arguments['<encodedImage>'])
        else:
            print("暂不支持此图片格式……")

        print("加密完成,密文为:\n"+arguments['<text>']+"\n")
    # 参数为-d则解密
    elif arguments['-d'] or arguments['decode']:
        print("解密中……\n")
        im = Image.open(arguments['<encodedImage>'])
        if im.mode == 'RGBA':
            print("解秘完成,密文为:\n"+RGBAdecodeImage(im)+"\n")
        else:
            print("非法的图片格式……")

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值