#region Steganography ## Extract data from an image. func extract_from_image(image : Image, bpc : Array[int]) -> PackedByteArray: assert(image and not image.is_compressed()) var data : PackedByteArray = image.get_data() return extract_from_byte_array(data, bpc, _get_ignored_channels(image)) ## Produce an altered version of the image with inserted data. func insert_to_image(image : Image, bpc : Array[int], data : PackedByteArray) -> Image: assert(image and not image.is_compressed() and _is_supported(image)) var src : PackedByteArray = image.get_data() var altered : PackedByteArray = insert_to_byte_array(src, bpc, data, _get_ignored_channels(image)) return Image.create_from_data(image.get_width(), image.get_height(), false, image.get_format(), altered) ## Extract a byte array from another one. func extract_from_byte_array(data : PackedByteArray, bpc : Array[int], ignored : Array[bool] = [ false, false, false, false ]) -> PackedByteArray: _assert_bpc(bpc, ignored) var channel_id : int = 0 var extracted : PackedByteArray = PackedByteArray() var bit : int = 0 var current : int = 0 for d : int in data: if ignored[channel_id]: channel_id = (channel_id + 1) & 0b11 continue var nb_bits : int = bpc[channel_id] channel_id = (channel_id + 1) & 0b11 if nb_bits == 0: continue var sub : int = d & ((~(0xFF << nb_bits)) & 0xFF) # At best, destination can contain the whole 'sub'. At worst, it's split on two bytes. current = current | (sub << bit) if (bit + nb_bits) & 0b1000 != 0: var first : int = 8 - bit current = current & 0xFF extracted.append(current) current = sub >> first bit = nb_bits - first else: bit += nb_bits if nb_bits == 8: extracted.append(current) current = 0 bit = 0 if bit > 0: extracted.append(current) return extracted ## Produce an altered version of the specified byte array where other data are integrated in it. func insert_to_byte_array(dest : PackedByteArray, bpc : Array[int], data : PackedByteArray, ignored : Array[bool] = [ false, false, false, false ]) -> PackedByteArray: _assert_bpc(bpc, ignored) var result : PackedByteArray = PackedByteArray() var channel_id : int = 0 var bit : int = 8 var byte : int = 0 var current : int = data[0] for di : int in dest: if ignored[channel_id] or byte == data.size(): channel_id = (channel_id + 1) & 0b11 result.append(di) continue var nb_bits : int = bpc[channel_id] channel_id = (channel_id + 1) & 0b11 if nb_bits == 0: result.append(di) continue # At best, source contains enough bits, at worst, we'll need to fetch from next byte. var mask : int = ((~(0xFF << nb_bits)) & 0xFF) var to_insert : int = current & mask if nb_bits > bit: # Fetch additional data var additional : int = nb_bits - bit if byte + 1 < data.size(): byte += 1 current = data[byte] var sub : int = current & ((~(0xFF << additional)) & 0xFF) to_insert = to_insert | (sub << bit) bit = 8 - additional current = current >> additional else: bit = 8 else: # Use existing. current = current >> nb_bits bit -= nb_bits if bit == 0: bit = 8 result.append((di & (~mask)) | to_insert) if bit == 8: byte += 1 if byte == data.size(): continue current = data[byte] return result ## Get supported format func _is_supported(image : Image) -> bool: return [ Image.FORMAT_L8, Image.FORMAT_R8, Image.FORMAT_LA8, Image.FORMAT_RG8, Image.FORMAT_RGB8, Image.FORMAT_RGBA8 ].has(image.get_format()) ## Compute ignored channels based on the image format. func _get_ignored_channels(image : Image) -> Array[bool]: match image.get_format(): Image.FORMAT_L8, Image.FORMAT_R8: return [ false, true, true, true ] Image.FORMAT_LA8: return [ false, true, true, false ] Image.FORMAT_RG8: return [ false, false, true, true ] Image.FORMAT_RGB8: return [ false, false, false, true ] _: return [ false, false, false, false ] ## Assert that Bits Per Channel array and ignored channels array are well formed. func _assert_bpc(bpc : Array[int], ignored : Array[bool]) -> void: assert(bpc and ignored and bpc.size() == 4 and ignored.size() == 4) var v : bool = true for b : bool in ignored: v = v && b assert(not v) # Can't ignore all for b : int in bpc: assert(b >= 0 and b <= 8) # extracted bits number must be in [0, 8] func _print_byte_array_as_hex(data : PackedByteArray) -> String: var result : String = "" for d : int in data: result += "%02X " % (d) return result func _print_byte_array(data : PackedByteArray) -> String: var result : String = "" for d : int in data: for i : int in range(7, -1, -1): result += "1" if ((d >> i) & 1 == 1) else "0" result += " " return result func _print_annotated_byte_array(data : PackedByteArray, bpc : Array[int], ignored : Array[bool]) -> String: var result : String = "" var channel_id : int = 0 for d : int in data: for i : int in range(7, -1, -1): if not ignored[channel_id] and bpc[channel_id] == (i + 1): result += "[b]" result += "1" if ((d >> i) & 1 == 1) else "0" if not ignored[channel_id] and bpc[channel_id] > 0: result += "[/b]" channel_id += 1; if channel_id == 4: channel_id = 0 result += " " return result #endregion #region Unit Test func _test() -> void: randomize() var test : PackedByteArray = PackedByteArray() var host_data_size : int = 32 for n : int in range(host_data_size): test.append(randi_range(0, 255)) var bpc : Array[int] = [randi_range(1, 4) , randi_range(1, 5), randi_range(1, 3), randi_range(1, 4)] var ignore : Array[bool] = [ false, false, true, false ] var to_insert : PackedByteArray = PackedByteArray() var max_data_size : int = 0 for i : int in range(4): if not ignore[i]: max_data_size += bpc[i] @warning_ignore("integer_division") max_data_size = (max_data_size * (host_data_size / 4)) / 8 for n : int in range(max_data_size): to_insert.append(randi_range(0, 255)) print("Reference byte array :") print(_print_byte_array(test)) print("Data to insert :") print(_print_byte_array(to_insert)) var modified : PackedByteArray = insert_to_byte_array(test, bpc, to_insert, ignore) print("Modified byte array :") print_rich(_print_annotated_byte_array(modified, bpc, ignore)) print("Extracted data from modified byte array :") var extracted : PackedByteArray = extract_from_byte_array(modified, bpc, ignore) print(_print_byte_array(extracted)) # Extracted data can be longer that inserted data,especially is the insert data underflow the source. assert(extracted.size() >= to_insert.size()) for i : int in range(to_insert.size()): assert(extracted[i] == to_insert[i]) #endregion
or share this direct link: