def decode(infile): """ Decode the data from a given file or file-like object. :param infile: File path or file-like object. :return: The encoded data as a byte array. """ encoded_image = Image.open(infile) # If this is a multi-channel image, the call to getdata() will return # an array of tuples for the pixel data. If this is a single-channel # image, we just get an array of pixel values. To normalize this, we # will try to flatten the array first. If this throws a TypeError, we # can just assume we have a single-channel image. try: encoded_data = encoded_image.getdata() encoded_data = itertools.chain.from_iterable(encoded_data) encoded_data = bytearray(encoded_data) except TypeError: encoded_data = encoded_image.getdata() encoded_data = bytearray(encoded_data) decoded_byte = 0 decoded_bytes = bytearray() current_byte = 0 # Find the size of the encoded data from the first N bytes of the image size_bytes = struct.calcsize('l') * 8 for i in xrange(size_bytes): if bits.is_bit_set(encoded_data[current_byte], 0): decoded_byte = bits.set_bit(decoded_byte, 7 - (i % 8)) else: decoded_byte = bits.unset_bit(decoded_byte, 7 - (i % 8)) if 7 - (i % 8) == 0: decoded_bytes.append(decoded_byte) current_byte += 1 decoded_size, = struct.unpack('l', str(decoded_bytes)) decoded_size *= 8 decoded_bytes = bytearray() decoded_byte = 0 # Decode the original datas for i in xrange(decoded_size): if bits.is_bit_set(encoded_data[current_byte], 0): decoded_byte = bits.set_bit(decoded_byte, 7 - (i % 8)) else: decoded_byte = bits.unset_bit(decoded_byte, 7 - (i % 8)) if 7 - (i % 8) == 0: decoded_bytes.append(decoded_byte) current_byte += 1 return decoded_bytes
def encode(data, infile, outfile): """ Encode the given data in a new copy of a given image file. :param data: Data to be encoded. This is typically a bytearray. :param infile: The original image to use. This can be either a file name or an open file object. :param outfile: The new image to create. This can be either a file name of an open file object. """ base_image = Image.open(infile) # If this is a multi-channel image, the call to getdata() will return # an array of tuples for the pixel data. If this is a single-channel # image, we just get an array of pixel values. To normalize this, we # will try to flatten the array first. If this throws a TypeError, we # can just assume we have a single-channel image. try: composite_data = base_image.getdata() composite_data = itertools.chain.from_iterable(composite_data) composite_data = bytearray(composite_data) except TypeError: composite_data = base_image.getdata() composite_data = bytearray(composite_data) # Build a simple header that will hold the size of the encoded data. # We use a structure here to make sure the header size itself remains # consistent. encoding_header = struct.pack('l', len(data)) # Prepend the header to the original data full_data = bytearray(encoding_header) full_data.extend(bytearray(data)) current_byte = 0 # Iterate over each byte in the input data. for data_byte in full_data: # Iterate over each bit in the input data byte in high-to-low order. for bit_shift in xrange(7, -1, -1): # If the current input bit is set, set the low bit in the current byte # of the base image. Similarly, if the bit is unset, then unset the low # bit of the current image byte. if bits.is_bit_set(data_byte, bit_shift): composite_data[current_byte] = bits.set_bit(composite_data[current_byte], 0) else: composite_data[current_byte] = bits.unset_bit(composite_data[current_byte], 0) # Go on to the next byte in the base image current_byte += 1 # Re-encode the data into a new image data_image_size = base_image.size composite_image = Image.frombytes(base_image.mode, data_image_size, str(composite_data)) # Save the image to the supplied output file composite_image.save(outfile, format=base_image.format)