diff --git a/config.json b/config.json new file mode 100644 index 0000000..71abf92 --- /dev/null +++ b/config.json @@ -0,0 +1,5 @@ +{ + "resolution": [1920, 1080], + "scale": 20, + "temp_folder": "temp/frames" +} diff --git a/decode.py b/decode.py new file mode 100755 index 0000000..943d187 --- /dev/null +++ b/decode.py @@ -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 [output_folder]") + sys.exit(1) + gif = sys.argv[1] + outf = sys.argv[2] if len(sys.argv) > 2 else "." + decode(gif, outf) + diff --git a/encode.py b/encode.py new file mode 100755 index 0000000..1356586 --- /dev/null +++ b/encode.py @@ -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 [output.gif]") + sys.exit(1) + inp = sys.argv[1] + out = sys.argv[2] if len(sys.argv) > 2 else None + encode(inp, out) diff --git a/main.py b/main.py old mode 100644 new mode 100755 index b753fa8..ab49961 --- a/main.py +++ b/main.py @@ -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= 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): @@ -380,4 +410,4 @@ def main(): #conversion_test() if __name__ == '__main__': - main() \ No newline at end of file + main()