From c417660f3d806dde0372408f52d7892de7f7deae Mon Sep 17 00:00:00 2001 From: Sameguyy Date: Thu, 30 Oct 2025 16:30:30 +0300 Subject: [PATCH 1/6] encoder\decoder --- decode.py | 41 +++++++++++++++++++++++++++++++++++++++++ encode.py | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ main.py | 54 +++++++++++++++++++++++------------------------------- 3 files changed, 113 insertions(+), 31 deletions(-) create mode 100644 decode.py create mode 100644 encode.py mode change 100644 => 100755 main.py diff --git a/decode.py b/decode.py new file mode 100644 index 0000000..9630bd0 --- /dev/null +++ b/decode.py @@ -0,0 +1,41 @@ +# decode.py +import sys +import os +from main import png_2_pixels, pixels_2_bits, bits_2_file +from PIL import Image +import imageio +import shutil + +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_file=None): + frames_folder = extract_frames_from_gif(gif_path) + bits = [] + # iterate frames in sorted order + items = sorted(os.listdir(frames_folder)) + for fname in items: + if not fname.endswith(".png"): + continue + pixels = png_2_pixels(os.path.join(frames_folder, fname)) + bits += pixels_2_bits(pixels) + out_path = output_file or (os.path.splitext(gif_path)[0] + ".recovered") + bits = bits[:len(bits) - (len(bits) % 8)] # trim to full bytes + bits_2_file(bits, out_path) + print("Recovered file:", out_path) + return out_path + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: python decode.py [output_file]") + sys.exit(1) + gif = sys.argv[1] + out = sys.argv[2] if len(sys.argv) > 2 else None + decode(gif, out) diff --git a/encode.py b/encode.py new file mode 100644 index 0000000..5547270 --- /dev/null +++ b/encode.py @@ -0,0 +1,49 @@ +# encode.py +import sys +import os +from main import file_2_bits, bits_2_pixels, pixels_2_png, make_gif + +def encode(input_path, out_gif=None, frame_width=3840, frame_height=2160): + # read bits from file + bits = file_2_bits(input_path) + + # pad bits to multiple of frame pixel count + frame_size = frame_width * frame_height + total_bits = len(bits) + # use 1 bit per pixel; compute number of frames + num_frames = (total_bits + frame_size - 1) // 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)) + 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..0997c4f --- a/main.py +++ b/main.py @@ -71,39 +71,31 @@ def png_2_pixels(fname): return pixel_list # 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 +372,4 @@ def main(): #conversion_test() if __name__ == '__main__': - main() \ No newline at end of file + main() From 65d6f8d8ab0ec509eefd87db04ca5cb72079d8dd Mon Sep 17 00:00:00 2001 From: Sameguyy Date: Fri, 31 Oct 2025 04:04:25 +0300 Subject: [PATCH 2/6] encoder --- encode.py | 49 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 8 deletions(-) mode change 100644 => 100755 encode.py diff --git a/encode.py b/encode.py old mode 100644 new mode 100755 index 5547270..1356586 --- a/encode.py +++ b/encode.py @@ -1,17 +1,48 @@ -# encode.py +#!/usr/bin/env python3 import sys import os -from main import file_2_bits, bits_2_pixels, pixels_2_png, make_gif +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}") -def encode(input_path, out_gif=None, frame_width=3840, frame_height=2160): # read bits from file bits = file_2_bits(input_path) - # pad bits to multiple of frame pixel count - frame_size = frame_width * frame_height + # 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) - # use 1 bit per pixel; compute number of frames 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" @@ -31,14 +62,16 @@ def encode(input_path, out_gif=None, frame_width=3840, frame_height=2160): 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)) + pixels_2_png(pixels, fname, reso=(frame_width, frame_height), scale=scale) + print("Wrote", fname) - # make gif + # 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: From 4390ddeba1276874c58e03071f463d113b375e63 Mon Sep 17 00:00:00 2001 From: Sameguyy Date: Fri, 31 Oct 2025 04:09:52 +0300 Subject: [PATCH 3/6] decoder --- decode.py | 70 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 49 insertions(+), 21 deletions(-) mode change 100644 => 100755 decode.py diff --git a/decode.py b/decode.py old mode 100644 new mode 100755 index 9630bd0..943d187 --- a/decode.py +++ b/decode.py @@ -1,10 +1,17 @@ -# decode.py -import sys +#!/usr/bin/env python3 import os -from main import png_2_pixels, pixels_2_bits, bits_2_file -from PIL import Image -import imageio +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): @@ -16,26 +23,47 @@ def extract_frames_from_gif(gif_path, out_folder="temp/frames_dec"): imageio.imwrite(fname, frame) return out_folder -def decode(gif_path, output_file=None): - frames_folder = extract_frames_from_gif(gif_path) - bits = [] - # iterate frames in sorted order - items = sorted(os.listdir(frames_folder)) - for fname in items: - if not fname.endswith(".png"): +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 - pixels = png_2_pixels(os.path.join(frames_folder, fname)) - bits += pixels_2_bits(pixels) - out_path = output_file or (os.path.splitext(gif_path)[0] + ".recovered") - bits = bits[:len(bits) - (len(bits) % 8)] # trim to full bytes - bits_2_file(bits, out_path) - print("Recovered file:", out_path) + 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_file]") + print("Usage: python decode.py [output_folder]") sys.exit(1) gif = sys.argv[1] - out = sys.argv[2] if len(sys.argv) > 2 else None - decode(gif, out) + outf = sys.argv[2] if len(sys.argv) > 2 else "." + decode(gif, outf) + From e29edd230232d5fac744e8d6e7e0024691665e77 Mon Sep 17 00:00:00 2001 From: Sameguyy Date: Fri, 31 Oct 2025 04:14:59 +0300 Subject: [PATCH 4/6] Base --- main.py | 135 ++++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 92 insertions(+), 43 deletions(-) diff --git a/main.py b/main.py index 0997c4f..308102e 100755 --- a/main.py +++ b/main.py @@ -14,6 +14,9 @@ # for deleting folders from shutil import rmtree +# for gif fps edit +#from imageio import imread, mimsave + four_k = (3840,2160) HD = (1920,1080) @@ -21,54 +24,100 @@ # 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): From ff18fde84640d3e22d91879f770dc9fac3aff22c Mon Sep 17 00:00:00 2001 From: Sameguyy Date: Fri, 31 Oct 2025 04:23:20 +0300 Subject: [PATCH 5/6] customize frame layout --- config.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 config.json 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" +} From 8f120a6923585fd4478d39f4b6dedf8f77c40f6e Mon Sep 17 00:00:00 2001 From: Sameguyy Date: Fri, 31 Oct 2025 04:47:05 +0300 Subject: [PATCH 6/6] Base --- main.py | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/main.py b/main.py index 308102e..ab49961 100755 --- a/main.py +++ b/main.py @@ -14,9 +14,6 @@ # for deleting folders from shutil import rmtree -# for gif fps edit -#from imageio import imread, mimsave - four_k = (3840,2160) HD = (1920,1080) @@ -33,24 +30,16 @@ def make_gif(parent_folder, fname): image = imageio.imread(os.path.join(parent_folder, filename)) writer.append_data(image) return fname + ".gif" - - with imageio.get_writer(fname + ".gif", mode="I", duration=duration) 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 from PIL import Image, ImageDraw -from PIL import Image, ImageDraw - def pixels_2_png(pixels, fname, reso=(1920, 1080), scale=1): """ - Создаёт изображение, где каждый пиксель (бит данных) - отрисовывается квадратом размера scale×scale пикселей. - Черный квадрат = 0, белый = 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