Skip to content

HideRGB

stegobox.codec.HideRGB

Bases: 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.

Source code in stegobox/codec/hide_rgb.py
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

encode(carrier, payload)

Encode byte type of file in rgb image.

Parameters:

Name Type Description Default
carrier Image

Carrier image in format png.

required
payload str

Secret message path.

required

Returns:

Type Description
Image

Encoded stego image.

Source code in stegobox/codec/hide_rgb.py
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

decode(carrier)

Decode sercet message from encoded image.

Parameters:

Name Type Description Default
carrier Image

Encoded image.

required

Returns:

Type Description
bytes

Decoded message in byte type.

Source code in stegobox/codec/hide_rgb.py
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