class HideRGB(BaseCodec):
"""
HideRGB: Hide byte type of file, e.g. PDF, in PNG.
* Created by: Jiayao Yang
* Created time: 2022/12/9
Source code derived from:
[mKotoulas/Hidden-Beauty](https://github.com/mKotoulas/Hidden-Beauty).
"""
def __init__(self) -> None:
super().__init__()
def _lsb_count(self, channels: int, file_size: int) -> int:
if file_size == channels:
return 1
else:
return file_size // channels + 1
def _store_bits(self, channel: int, values: str, nbits: int) -> int:
mask = 255 - (2**nbits - 1)
return (channel & mask) | (int(values, 2))
def _retrieve_bits(self, channel: int, n_bits: int) -> str:
return (str(bin(channel)[2:]).zfill(8))[-n_bits:]
def _injecy_file(self, file: bytes, filename: bytes, target_image: np.ndarray):
# 1-D array of integers representing channels values (0-255)
img_arr_flat = target_image.flatten()
# number of rgb channels in the picture
total_channels = len(img_arr_flat)
# rgb channels left for storing the filesize and the file
channels_after_fn = total_channels - (256 * 8 + 1)
# max file size the pic can store in bits
max_file_size = 4 * channels_after_fn
# maximum rgb channels dedicated to store the maximum
# filesize.Size of the file is stored as a binary number
# representing the number of bits in the file.
channels_for_size = len(bin(max_file_size)[2:])
# number of channels available for storing the file
available_channels = channels_after_fn - channels_for_size
# size of the file in bits
file_size_inbits = len(file) * 8
bits_to_alter = self._lsb_count(available_channels, file_size_inbits)
if bits_to_alter > 4:
exit(
f"File size is too big...\n"
f"You can store a maximum of "
f"{round((available_channels*4)/(8*10**6),4)} MB in that png!"
)
# channels used
channels_used_float = file_size_inbits / bits_to_alter
if file_size_inbits % bits_to_alter == 0:
channels_used = int(channels_used_float)
else:
channels_used = int(channels_used_float) + 1
# channels left for random values
last_channel = ceil(bits_to_alter * (channels_used_float % 1))
lsb_last_channel = last_channel if last_channel != 0 else bits_to_alter
# convert file in binary format
bin_file = "".join(f"{x:0>8b}" for x in file)
# convert filename in binary format
bin_fn = "".join(f"{x:0>8b}" for x in filename).zfill(256 * 8)
bin_size = str(bin(file_size_inbits)[2:]).zfill(channels_for_size)
index = str(bin(bits_to_alter)[2:]).zfill(3)
# store index
# print("[green]Storing index...")
img_arr_flat[0] = self._store_bits(img_arr_flat[0], index, 3)
idx = 1
# store filename
# ("[green]Storing filename...")
for i, h in enumerate(bin_fn):
img_arr_flat[idx + i] = self._store_bits(img_arr_flat[idx + i], h, 1)
idx += 256 * 8
# store file_size(binary)
# print("[green]Storing file...")
for i, h in enumerate(bin_size):
img_arr_flat[idx + i] = self._store_bits(img_arr_flat[idx + i], h, 1)
idx += channels_for_size
# store file
file_idx = 0
end_loop = idx + (channels_used - 1)
i = 0
for i in range(idx, end_loop):
img_arr_flat[i] = self._store_bits(
img_arr_flat[i],
bin_file[file_idx : (file_idx + bits_to_alter)],
bits_to_alter,
)
file_idx += bits_to_alter
idx = i + 1
# handle the last channel
img_arr_flat[idx] = self._store_bits(
img_arr_flat[idx], bin_file[file_idx:], lsb_last_channel
)
idx += 1
# Shape and save the modified png
img_arr = img_arr_flat.reshape(target_image.shape)
im = Image.fromarray(img_arr)
# im.save(f"{MODIFIED_IMAGES_PATH}/mod_img_{counter+1}.png")
return im
def _extract_file(self, mod_img_arr: np.array) -> dict[str, bytes]: # type: ignore
mod_img_arr_flat = mod_img_arr.flatten() # type: ignore
total_channels = len(mod_img_arr_flat)
channels_after_fn = total_channels - (256 * 8 + 1)
max_file_size = 4 * channels_after_fn
channels_for_size = len(bin(max_file_size)[2:])
# print("[green]Extracting...")
# restore number of lsb changed
bits_to_alter = int(self._retrieve_bits(mod_img_arr_flat[0], 3), 2)
idx = 1
# restore filename
filename = "".join(
[
self._retrieve_bits(mod_img_arr_flat[i], 1)
for i in range(idx, 256 * 8 + 1)
]
)
idx += 256 * 8
# restore file_size
file_size_inbits = int(
"".join(
[
self._retrieve_bits(mod_img_arr_flat[i], 1)
for i in range(idx, idx + channels_for_size)
]
),
2,
)
idx += channels_for_size # points to first channel that holds the file
channels_used_float = file_size_inbits / bits_to_alter
if file_size_inbits % bits_to_alter == 0:
channels_used = int(channels_used_float)
else:
channels_used = int(channels_used_float) + 1
last_channel = ceil(bits_to_alter * (channels_used_float % 1))
if last_channel != 0:
lsb_last_channel = last_channel
else:
lsb_last_channel = bits_to_alter
# restore file
file = "".join(
[
self._retrieve_bits(mod_img_arr_flat[i], bits_to_alter)
for i in range(idx, idx + channels_used - 1)
]
)
idx = idx + channels_used
# handle last channel
file_last_bits = self._retrieve_bits(mod_img_arr_flat[idx], lsb_last_channel)
# merge
final_file = file + file_last_bits
filename_hex = f"{int(filename,2):x}"
filename_bytes = bytes.fromhex(filename_hex)
file_hex = f"{int(final_file,2):x}"
file_bytes = bytes.fromhex(file_hex)
return {"filename": filename_bytes, "file": file_bytes}
def encode(self, carrier: Image.Image, payload: str) -> Image.Image:
"""Encode byte type of file in rgb image.
Args:
carrier: Carrier image in format png.
payload: Secret message path.
Returns:
Encoded stego image.
"""
with open(payload, "rb") as f:
payload_file = f.read() # type: ignore
filename = Path(payload).name
filename_bytes = filename.encode("utf-8")
img_array = np.array(carrier)
encoded = self._injecy_file(payload_file, filename_bytes, img_array)
return encoded
def decode(self, carrier: Image.Image) -> bytes:
"""Decode sercet message from encoded image.
Args:
carrier: Encoded image.
Returns:
Decoded message in byte type.
"""
mod_img_arr = np.array(carrier)
content = self._extract_file(mod_img_arr)
file = content["file"]
return file