适用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("非法的图片格式……")