commit ab1c5d9128c9745332474b519c914fe1c0accf1d
parent ee3fc9cf5720fe687fc13c8331c768d6444d7741
Author: andrew.laack <andrew.laack@imbue.com>
Date: Sat, 13 Sep 2025 23:21:26 -0700
Added unlimited for python.
Diffstat:
| A | python/decode.py | | | 76 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | python/encode.py | | | 138 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
2 files changed, 214 insertions(+), 0 deletions(-)
diff --git a/python/decode.py b/python/decode.py
@@ -0,0 +1,76 @@
+import numpy as np
+import os
+from PIL import Image
+
+class Header:
+ def __init__(self, name, chunk, bytes_enc):
+ self.name = name
+ self.chunk = chunk
+ self.bytes_enc = bytes_enc
+ def __str__(self):
+ return f"Name: {self.name} Chunk Number: {self.chunk} Bytes Encoded: {self.bytes_enc}"
+ def __lt__(self, other):
+ if self.name != other.name:
+ return self.name < other.name
+ else:
+ return self.chunk < other.chunk
+
+def bits_to_byte(bits):
+ return sum(bit << (7 - i) for i, bit in enumerate(bits))
+
+def get_header(image_path):
+ img = Image.open(image_path).convert('L')
+ pixels = list(img.getdata())
+
+ bits = [1 if px == 255 else 0 for px in pixels[:864]]
+ header_bytes = bytearray()
+ for i in range(0, 864, 8):
+ header_bytes.append(bits_to_byte(bits[i:i+8]))
+
+ file_name_bytes = header_bytes[0:100]
+ chunk_number_bytes = header_bytes[100:104]
+ bytes_encoded_bytes = header_bytes[104:108]
+
+ file_name = file_name_bytes.rstrip(b'\x00').decode('utf-8', errors='replace').replace('\x00', '')
+ chunk_number = int(np.frombuffer(chunk_number_bytes, dtype=np.int32)[0])
+ bytes_encoded = int(np.frombuffer(bytes_encoded_bytes, dtype=np.int32)[0])
+
+ return Header(file_name, chunk_number, bytes_encoded)
+
+def extract_data_from_image(image_path, bytes_to_read):
+ img = Image.open(image_path).convert('L')
+ pixels = list(img.getdata())
+
+ data_pixels = pixels[864:]
+
+ bits = [1 if px > 127 else 0 for px in data_pixels]
+
+ data_bytes = bytearray()
+ total_bits_needed = bytes_to_read * 8
+
+ for i in range(0, total_bits_needed, 8):
+ byte_bits = bits[i:i+8]
+ if len(byte_bits) < 8:
+ byte_bits += [0] * (8 - len(byte_bits))
+ data_bytes.append(bits_to_byte(byte_bits))
+
+ return data_bytes
+
+files = os.listdir()
+headers = []
+
+for file in files:
+ if file.endswith(".png"):
+ header = get_header(file)
+ headers.append((header, file))
+
+headers.sort()
+
+for header, file in headers:
+ print(f"Decoding: {header}")
+
+ data = extract_data_from_image(file, header.bytes_enc)
+
+ mode = 'wb' if header.chunk == 1 else 'ab'
+ with open(header.name, mode) as f:
+ f.write(data)
diff --git a/python/encode.py b/python/encode.py
@@ -0,0 +1,138 @@
+import numpy as np
+import math
+import os
+import sys
+from PIL import Image
+import random
+import string
+
+def get_rnd_str(length):
+ random_string = ''.join(random.choices(string.ascii_letters + string.digits, k=length))
+ return(random_string)
+
+# idea:
+# encode text into XxX matrix for white/black bits
+# use image magick or smt
+# ffmpeg -> vid
+
+
+def byte_to_bits(byte):
+ return [(byte >> i) & 1 for i in reversed(range(8))]
+
+
+# assume 1920x1080
+# how many bits?
+# 2073600 bits!
+# 259,200 bytes
+
+# this should be a deterministic size for ease of encoding/decoding
+
+# reserved header:
+# file name - 100 bytes - 800 bits / pixels
+# chunk number - 4 bytes - 32 pixels
+# bytes encoded - 4 bytes - 32 pixels
+
+# 108 * 8 = 864
+
+def make_header(file_name, chunk_number, bytes_encoded):
+ file_name_bytes = file_name.encode("utf-8")
+ if len(file_name_bytes) > 100:
+ file_name_bytes = file_name_bytes[:100]
+ else:
+ file_name_bytes = file_name_bytes.ljust(100, b'\x00')
+
+ chunk_num = np.int32(chunk_number).tobytes()
+ num_bytes = np.int32(bytes_encoded).tobytes()
+
+ header = file_name_bytes + chunk_num + num_bytes
+ assert len(header) == 108
+ return header
+
+# reserved header is 108 bytes
+
+# 259,200 - 108 = 259,092
+
+x = int(float(sys.argv[1]))
+y = int(float(sys.argv[2]))
+total_bits_available = x * y
+bits_for_data = total_bits_available - 864
+bytes_per_frame = bits_for_data // 8
+
+print("Max bytes per frame: " + str(bytes_per_frame))
+
+filename = sys.argv[3]
+destination = sys.argv[4]
+destination.removesuffix("/")
+
+if not os.path.exists(destination):
+ os.mkdir(destination)
+
+print("x: " + str(x))
+print("y: " + str(y))
+print("filename: " + filename)
+
+file_size_bytes = os.path.getsize(filename)
+print("File size in bytes: " + str(file_size_bytes))
+
+
+chunks_required = math.ceil(file_size_bytes / bytes_per_frame)
+print("Chunks required: " + str(chunks_required))
+
+if chunks_required > 1:
+ bytes_per_chunk = [bytes_per_frame] * (chunks_required - 1)
+ bytes_per_chunk.append(file_size_bytes % bytes_per_frame)
+else:
+ bytes_per_chunk = [file_size_bytes]
+
+assert sum(bytes_per_chunk) == file_size_bytes
+
+print("Bytes per chunk: " + str(bytes_per_chunk))
+
+
+with open(filename, "rb") as file:
+ file_bytes = file.read()
+
+# ENSURE HEADER IS 108 BYTES
+# SIDE EFFECT FOR ARR
+# BYTES TO WRITE IS NUM
+# BYTES STR IS DATA
+def create_image(arr, header, byte_str, start_idx, bytes_to_write, x):
+
+ written = 0
+
+ offset = 0
+ for byte in header:
+ for bit in byte_to_bits(byte):
+ x_idx = offset % x
+ y_idx = offset // x
+ arr[y_idx][x_idx] = 255 if bit else 0
+ offset += 1
+ written += 1
+
+ assert offset == 108 * 8
+
+ for i in range(bytes_to_write):
+ byte = byte_str[start_idx + i]
+ for bit in byte_to_bits(byte):
+ x_idx = offset % x
+ y_idx = offset // x
+ arr[y_idx][x_idx] = 255 if bit else 0
+ written += 1
+ offset += 1
+ return written
+
+
+index = 0
+chunk_num = 1
+for chunk_size in bytes_per_chunk:
+ arr = np.zeros((y,x))
+ written = create_image(arr, make_header(filename, chunk_num, chunk_size), file_bytes, index, chunk_size, x)
+ img = Image.fromarray(arr)
+ img = img.convert('L')
+ fname = destination + "/" + str(get_rnd_str(16)) + ".png"
+ print("Writing to: " + fname)
+ img.save(fname)
+ index += chunk_size
+ chunk_num += 1
+
+assert index == file_size_bytes