Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"resolution": [1920, 1080],
"scale": 20,
"temp_folder": "temp/frames"
}
69 changes: 69 additions & 0 deletions decode.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
import os
import sys
import json
import shutil
import imageio
from main import png_2_pixels, pixels_2_bits, bits_2_file, decode_header, file_2_bits
from PIL import Image

def load_config(path="config.json"):
script_dir = os.path.dirname(os.path.abspath(__file__))
full_path = os.path.join(script_dir, path)
with open(full_path, "r") as f:
return json.load(f)

def extract_frames_from_gif(gif_path, out_folder="temp/frames_dec"):
if os.path.exists(out_folder):
shutil.rmtree(out_folder)
os.makedirs(out_folder)
vid = imageio.get_reader(gif_path)
for i, frame in enumerate(vid):
fname = os.path.join(out_folder, "frame-%05d.png" % i)
imageio.imwrite(fname, frame)
return out_folder

def decode(gif_path, output_folder=".", temp_folder="temp/frames_dec"):
cfg = load_config()
scale = cfg.get("scale", 1)

frames_folder = extract_frames_from_gif(gif_path, temp_folder)

# read logical pixels from each frame using png_2_pixels with scale
logical_pixels = []
frame_files = sorted(os.listdir(frames_folder))
for fname in frame_files:
if not fname.lower().endswith(".png"):
continue
full = os.path.join(frames_folder, fname)
pixels = png_2_pixels(full, scale=scale)
logical_pixels.extend(pixels)

# convert logical pixels to bits
bits = pixels_2_bits(logical_pixels)
# diagnostic: show first 128 bits as bytes
sample = bits[:128]
byts = ['{0:08b}'.format(int(''.join(sample[i:i+8]),2)) for i in range(0, len(sample), 8)]
print("DEBUG first bytes (bin):", ' '.join(byts))
print("DEBUG first bytes (hex):", ' '.join(hex(int(b,2)) for b in byts))


# decode header to get filename and payload bits
fname, payload_bits = decode_header(bits)

out_name = os.path.splitext(fname)[0] + "-recovered." + os.path.splitext(fname)[1]
out_path = os.path.join(output_folder, out_name)

# write payload bits to file
bits_2_file(payload_bits, out_path)
print("Decoded and wrote:", out_path)
return out_path

if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python decode.py <input.gif> [output_folder]")
sys.exit(1)
gif = sys.argv[1]
outf = sys.argv[2] if len(sys.argv) > 2 else "."
decode(gif, outf)

82 changes: 82 additions & 0 deletions encode.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/usr/bin/env python3
import sys
import os
from main import file_2_bits, bits_2_pixels, pixels_2_png, make_gif, add_header
import json

def load_config(path="config.json"):
script_dir = os.path.dirname(os.path.abspath(__file__))
full_path = os.path.join(script_dir, path)
with open(full_path, "r") as f:
return json.load(f)

def encode(input_path, out_gif=None):
cfg = load_config()
frame_width, frame_height = cfg["resolution"]
scale = cfg["scale"]
temp_folder = cfg.get("temp_folder", "temp/frames")

print(f"Loaded config: res={frame_width}x{frame_height}, scale={scale}")

# read bits from file
bits = file_2_bits(input_path)

# diagnostic: show first 64 bits as bytes (useful to compare with decoder)
if len(bits) >= 64:
first_bytes = []
for i in range(0, 64, 8):
byte = int(''.join(bits[i:i+8]), 2)
first_bytes.append(hex(byte))
print("DEBUG: first bytes hex:", ' '.join(first_bytes))
else:
print("DEBUG: bits length <", len(bits))

# add header (filename + payload length) compatible with decode_header in main.py
bits = add_header(bits, os.path.basename(input_path).encode('utf-8'))

# Number of logical bits in a frame (Based on the specified scale)
logical_w = frame_width // scale
logical_h = frame_height // scale
frame_size = logical_w * logical_h

total_bits = len(bits)
num_frames = (total_bits + frame_size - 1) // frame_size
print(f"Frame logical grid: {logical_w}x{logical_h}, total bits per frame: {frame_size}")


# create temp folder
temp_folder = "temp/frames"
if os.path.exists(temp_folder):
# keep previous contents or clear
import shutil
shutil.rmtree(temp_folder)
os.makedirs(temp_folder)

# for each frame, take slice of bits, convert to pixels and save png
for i in range(num_frames):
start = i * frame_size
end = min(start + frame_size, total_bits)
frame_bits = bits[start:end]
# pad with zeros (black pixels)
if len(frame_bits) < frame_size:
frame_bits += ['0'] * (frame_size - len(frame_bits))
pixels = bits_2_pixels(frame_bits)
fname = os.path.join(temp_folder, "frame-%05d.png" % i)
pixels_2_png(pixels, fname, reso=(frame_width, frame_height), scale=scale)

print("Wrote", fname)

# make gif
base_out = out_gif[:-4] if out_gif and out_gif.endswith(".gif") else (out_gif or "out")
gif_path = make_gif(temp_folder, base_out)
print("Created:", gif_path)
return gif_path


if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python encode.py <input_file> [output.gif]")
sys.exit(1)
inp = sys.argv[1]
out = sys.argv[2] if len(sys.argv) > 2 else None
encode(inp, out)
178 changes: 104 additions & 74 deletions main.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -21,89 +21,119 @@
# 0 is black pixel, white is 1

# writes a gif in parent_folder made up of all it's sorted .png files
def make_gif(parent_folder,fname):
items = os.listdir(parent_folder)
png_filenames = []
for elem in items:
if elem.find(".png")!=-1:
png_filenames.append(elem)

sorted_png = []
while True:
lowest = 10000000
lowest_idx = -1
for p in png_filenames:
val = int(p.split("-")[1].split(".")[0])
if lowest_idx==-1 or val<lowest:
lowest = val
lowest_idx = png_filenames.index(p)
sorted_png.append(png_filenames[lowest_idx])
del png_filenames[lowest_idx]
if len(png_filenames)==0: break
png_filenames = sorted_png

with imageio.get_writer(fname+".gif", mode='I',duration=0.1) as writer:
for filename in png_filenames:
image = imageio.imread(parent_folder+"/"+filename)
writer.append_data(image)
return fname+".gif"
def make_gif(parent_folder, fname):
items = os.listdir(parent_folder)
png_filenames = sorted([f for f in items if f.endswith(".png")])

with imageio.get_writer(fname + ".gif", mode="I", duration=0.1) as writer:
for filename in png_filenames:
image = imageio.imread(os.path.join(parent_folder, filename))
writer.append_data(image)
return fname + ".gif"

# provided a list of pixels, writes it out as an image
# with the specified resolution
def pixels_2_png(pixels,fname,reso=four_k):
img = Image.new('RGB',reso)
img.putdata(pixels)
img.save(fname)
#print pixels[:16]
print("pixels_2_png: Saved to %d pixels to %s" % (len(pixels),fname))
from PIL import Image, ImageDraw

def pixels_2_png(pixels, fname, reso=(1920, 1080), scale=1):
"""
Creates an image where each pixel (bit of data)
is drawn as a square of scale×scale pixels.
Black square = 0, white square = 1.
"""
# Calculating the number of "logical pixels" along the axes
width, height = reso
logical_w = width // scale
logical_h = height // scale

img = Image.new("RGB", (width, height), (0, 0, 0))
draw = ImageDraw.Draw(img)

idx = 0
for y in range(logical_h):
for x in range(logical_w):
if idx >= len(pixels):
break
color = pixels[idx]
x0 = x * scale
y0 = y * scale
draw.rectangle([x0, y0, x0 + scale - 1, y0 + scale - 1], fill=color)
idx += 1

img.save(fname)
print(f"pixels_2_png: Saved {len(pixels)} bits as {logical_w}x{logical_h} logical grid, each {scale}px -> {fname}")


img.save(fname)
print(f"pixels_2_png: Saved {len(pixels)} pixels (scale={scale}x) to {fname}")


# provided a filename, reads the png and returns a list of pixels
def png_2_pixels(fname):
im = Image.open(fname)
pixel_list = []
pixels = im.load()
width,height = im.size
for row in range(height):
for col in range(width):
pixel_list.append(pixels[col,row])
print("png_2_pixels: Read %d pixels from %s" % (len(pixel_list),fname))
#pixels_2_png(pixel_list,"test2.png")
return pixel_list
from PIL import Image

def png_2_pixels(fname, scale=1):
im = Image.open(fname)
im = im.convert('RGB')
width, height = im.size

# if scale = 1, the behavior is default
if scale == 1:
pixels = list(im.getdata())
print(f"png_2_pixels: Read {len(pixels)} pixels from {fname}")
return pixels

logical_w = width // scale
logical_h = height // scale
pixels = []

for y in range(logical_h):
for x in range(logical_w):
# We take the average color of the square scale×scale
r_total = g_total = b_total = 0
for dy in range(scale):
for dx in range(scale):
px = im.getpixel((x * scale + dx, y * scale + dy))
r_total += px[0]
g_total += px[1]
b_total += px[2]
count = scale * scale
avg_r = r_total // count
avg_g = g_total // count
avg_b = b_total // count

# Deciding whether a square is black or white
color = (255, 255, 255) if avg_r > 127 else (0, 0, 0)
pixels.append(color)

print(f"png_2_pixels: Read {len(pixels)} logical pixels ({logical_w}x{logical_h}) from {fname} with scale={scale}")
return pixels

# writes out the bits as binary to a file
def bits_2_file(bits,fname):
f = open(fname,'wb')
idx=0
inc=8
while True:
char = ''.join(bits[idx:idx+inc])
f.write(chr(int(char,2)))
idx+=inc
if idx>=len(bits): break
f.close()
print("bits_2_file: Wrote %d bits to %s" % (len(bits),fname))
def bits_2_file(bits, fname):
with open(fname, 'wb') as f:
idx = 0
inc = 8
while idx < len(bits):
char = ''.join(bits[idx:idx+inc])
b = int(char, 2)
f.write(bytes([b]))
idx += inc
print("bits_2_file: Wrote %d bits to %s" % (len(bits), fname))

# returns a list of bits in the file
def file_2_bits(fname):
bits = []
f = open(fname, "rb")
try:
byte = f.read(1)
while byte != "":
cur_bits = bin(ord(byte))[2:]
while len(cur_bits)<8:
cur_bits = "0"+cur_bits
for b in cur_bits:
bits.append(b)
byte = f.read(1)
finally:
f.close()
'''
first_char = ''.join(bits[:8])
n = int(first_char,2)
print(binascii.unhexlify('%x' % n))
'''
return bits
bits = []
with open(fname, "rb") as f:
byte = f.read(1)
while byte != b"":
cur_bits = bin(byte[0])[2:]
while len(cur_bits) < 8:
cur_bits = "0" + cur_bits
for b in cur_bits:
bits.append(b)
byte = f.read(1)
return bits


# converts a list of 0/1 bits to pixels
def bits_2_pixels(bits):
Expand Down Expand Up @@ -380,4 +410,4 @@ def main():
#conversion_test()

if __name__ == '__main__':
main()
main()