Skip to content

LPSPNG

stegobox.codec.LPSPNG

Bases: BaseCodec

Linked Pixel Steganography (LPS) technique is a variant of the well-known LSB steganography.

Originally implemented in FlorianPicca/Linked-Pixel-Steganography

Source code in stegobox/codec/lps_png.py
class LPSPNG(BaseCodec):
    """Linked Pixel Steganography (LPS) technique is a variant of the well-known LSB
    steganography.

    Originally implemented in
    [FlorianPicca/Linked-Pixel-Steganography](https://github.com/FlorianPicca/Linked-Pixel-Steganography)
    """

    def __init__(self) -> None:
        self.start_x = 0
        self.start_y = 0
        super().__init__()

    def pixel_number_to_coordinate(self, n, img):
        return (n % img.size[0], n // img.size[0])

    def coordinate_to_pixel_number(self, x, y, img):
        return y * img.size[0] + x

    def set_lsb(self, v, state):
        if state == "0":
            return v & 0b11111110
        elif state == "1":
            return v | 0b00000001
        else:
            print(f"invalide state: {state}")
            return v

    def write(self, data, pixel, next_p, img):
        pix = img.load()
        x, y = self.pixel_number_to_coordinate(next_p, img)
        length = len(data)
        # binari representation of next pixel x
        col = bin(x)[2:].zfill(length)
        # binari representation of next pixel y
        lin = bin(y)[2:].zfill(length)

        for i in range(pixel, pixel + length):
            p = pix[self.pixel_number_to_coordinate(i, img)]
            if len(p) == 4:
                # With alpha channel
                pix[self.pixel_number_to_coordinate(i, img)] = (
                    self.set_lsb(p[0], data[i - pixel]),
                    self.set_lsb(p[1], col[i - pixel]),
                    self.set_lsb(p[2], lin[i - pixel]),
                    p[3],
                )
            else:
                # no alpha channel
                pix[self.pixel_number_to_coordinate(i, img)] = (
                    self.set_lsb(p[0], data[i - pixel]),
                    self.set_lsb(p[1], col[i - pixel]),
                    self.set_lsb(p[2], lin[i - pixel]),
                )

    def to_bin(self, string):
        return "".join(format(x, "b").zfill(8) for x in string)

    def chunkstring(self, string, length):
        return [
            string[0 + i : length + i].ljust(length, "0")
            for i in range(0, len(string), length)
        ]

    def get_data(self, img, start_x, start_y):
        n = self.coordinate_to_pixel_number(start_x, start_y, img)
        pix = img.load()
        blocklen = len(bin(max(img.size))[2:])
        nx = ""
        ny = ""
        s = ""
        for i in range(blocklen):
            c = self.pixel_number_to_coordinate(n + i, img)
            s += str(pix[c][0] & 1)
            nx += str(pix[c][1] & 1)
            ny += str(pix[c][2] & 1)
        nx = int(nx, 2)
        ny = int(ny, 2)
        return (s, (nx, ny))

    def bin_to_string(self, i):
        # pad i to be a multiple of 8
        if len(i) % 8 != 0:
            r = 8 - (len(i) % 8)
            i = i + "0" * r
        h = hex(int(i, 2))[2:]
        if len(h) % 2 != 0:
            h = "0" + h
        # remove last null byte
        return binascii.unhexlify(h)[:-1].decode()

    def encode(self, carrier: Image.Image, payload: str) -> Image.Image:
        """Encoder requires the carrier image to be PNG with Alpha channel and the
        payload to be a string.

        Args:
            carrier: Carrier image in format PNG. Read with 'stegobox.io.image.read()'.
            payload: Payload (secret message) to be encoded.

        Returns:
            Encoded image in format PNG with the payload embeded.
        """
        blocklen = len(bin(max(carrier.size))[2:])
        # The number of pixels in the image
        total = carrier.size[0] * carrier.size[1]
        # list of available block positions
        available = [x for x in range(1, total - 1, blocklen)]
        # Check if the last position is big enough
        if available[-1] + blocklen >= total:
            available.pop()

        d = self.chunkstring(self.to_bin(payload.encode()), blocklen)
        n = len(d)
        # choose the first pixel
        pixel = choice(available)
        available.remove(pixel)
        starting_pixel = self.pixel_number_to_coordinate(pixel, carrier)
        self.start_x = starting_pixel[0]
        self.start_y = starting_pixel[1]
        for i in range(n - 1):
            # pointer to the next pixel
            next_p = choice(available)
            available.remove(next_p)
            self.write(d[i], pixel, next_p, carrier)
            # switch to next pixel
            pixel = next_p
        # last pointer towards NULL (0, 0)
        self.write(d[-1], pixel, 0, carrier)
        return carrier

    def decode(self, carrier: Image.Image) -> str:
        """Decoder requires the encoded image in format PNG with the payload embeded.

        Args:
            carrier: Encoded carrier image.

        Returns:
            The decoded secret message.
        """
        data, p = self.get_data(carrier, self.start_x, self.start_y)
        while p != (0, 0):
            d, p = self.get_data(carrier, p[0], p[1])
            data += d
            print(d, p)
        return self.bin_to_string(data)

encode(carrier, payload)

Encoder requires the carrier image to be PNG with Alpha channel and the payload to be a string.

Parameters:

Name Type Description Default
carrier Image

Carrier image in format PNG. Read with 'stegobox.io.image.read()'.

required
payload str

Payload (secret message) to be encoded.

required

Returns:

Type Description
Image

Encoded image in format PNG with the payload embeded.

Source code in stegobox/codec/lps_png.py
def encode(self, carrier: Image.Image, payload: str) -> Image.Image:
    """Encoder requires the carrier image to be PNG with Alpha channel and the
    payload to be a string.

    Args:
        carrier: Carrier image in format PNG. Read with 'stegobox.io.image.read()'.
        payload: Payload (secret message) to be encoded.

    Returns:
        Encoded image in format PNG with the payload embeded.
    """
    blocklen = len(bin(max(carrier.size))[2:])
    # The number of pixels in the image
    total = carrier.size[0] * carrier.size[1]
    # list of available block positions
    available = [x for x in range(1, total - 1, blocklen)]
    # Check if the last position is big enough
    if available[-1] + blocklen >= total:
        available.pop()

    d = self.chunkstring(self.to_bin(payload.encode()), blocklen)
    n = len(d)
    # choose the first pixel
    pixel = choice(available)
    available.remove(pixel)
    starting_pixel = self.pixel_number_to_coordinate(pixel, carrier)
    self.start_x = starting_pixel[0]
    self.start_y = starting_pixel[1]
    for i in range(n - 1):
        # pointer to the next pixel
        next_p = choice(available)
        available.remove(next_p)
        self.write(d[i], pixel, next_p, carrier)
        # switch to next pixel
        pixel = next_p
    # last pointer towards NULL (0, 0)
    self.write(d[-1], pixel, 0, carrier)
    return carrier

decode(carrier)

Decoder requires the encoded image in format PNG with the payload embeded.

Parameters:

Name Type Description Default
carrier Image

Encoded carrier image.

required

Returns:

Type Description
str

The decoded secret message.

Source code in stegobox/codec/lps_png.py
def decode(self, carrier: Image.Image) -> str:
    """Decoder requires the encoded image in format PNG with the payload embeded.

    Args:
        carrier: Encoded carrier image.

    Returns:
        The decoded secret message.
    """
    data, p = self.get_data(carrier, self.start_x, self.start_y)
    while p != (0, 0):
        d, p = self.get_data(carrier, p[0], p[1])
        data += d
        print(d, p)
    return self.bin_to_string(data)