fvid/fvid/fvid.py

331 lines
8.6 KiB
Python
Raw Normal View History

2020-10-05 12:33:16 +02:00
from bitstring import Bits, BitArray
from PIL import Image
import glob
2020-10-09 20:13:06 +02:00
from operator import sub
2020-10-05 12:33:16 +02:00
import numpy as np
from tqdm import tqdm
import binascii
import argparse
import sys
import os
import getpass
import io
import gzip
import pickle
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from Crypto.Cipher import AES
try:
from fvid_cython import cy_get_bits_from_image as cy_gbfi
except (ImportError, ModuleNotFoundError):
use_cython = False
else:
use_cython = True
2020-10-05 12:33:16 +02:00
FRAMES_DIR = "./fvid_frames/"
SALT = '63929291bca3c602de64352a4d4bfe69'.encode() # It need be the same in one instance of coding/decoding
DEFAULT_KEY = ' '*32
DEFAULT_KEY = DEFAULT_KEY.encode()
NOTDEBUG = True
2020-10-05 12:33:16 +02:00
class WrongPassword(Exception):
pass
class MissingArgument(Exception):
pass
def get_password(password_provided):
if password_provided=='default':
return DEFAULT_KEY
else:
if password_provided == None:
password_provided = getpass.getpass("Enter password:")
password = str(password_provided).encode()
kdf = PBKDF2HMAC(
algorithm=hashes.SHA512(),
length=32,
salt=SALT,
iterations=100000,
backend=default_backend()
)
key = kdf.derive(password)
return key
2020-10-05 12:33:16 +02:00
def get_bits_from_file(filepath, key):
print('Reading file...')
bitarray = BitArray(filename=filepath)
2020-10-05 12:33:16 +02:00
# adding a delimiter to know when the file ends to avoid corrupted files
# when retrieving
cipher = AES.new(key, AES.MODE_EAX, nonce=SALT)
ciphertext, tag = cipher.encrypt_and_digest(bitarray.tobytes())
filename = os.path.basename(filepath)
pickled = pickle.dumps({'tag':tag,
'data':ciphertext,
'filename':filepath})
print('Ziping...')
#zip
out = io.BytesIO()
with gzip.GzipFile(fileobj=out, mode='w') as fo:
fo.write(pickled)
zip = out.getvalue()
#zip
del bitarray
del pickled
bitarray = BitArray(zip)
2020-10-05 12:33:16 +02:00
return bitarray.bin
2020-10-09 19:22:40 +02:00
def less(val1, val2):
return val1 < val2
2020-10-05 12:33:16 +02:00
def get_bits_from_image(image):
if use_cython:
bits = cy_gbfi(image)
return bits, False
2020-10-05 12:33:16 +02:00
width, height = image.size
done = False
px = image.load()
bits = ""
pbar = range(height)
white = (255, 255, 255)
black = (0, 0, 0)
2020-10-05 12:33:16 +02:00
for y in pbar:
for x in range(width):
pixel = px[x, y]
pixel_bin_rep = "0"
2020-10-05 12:33:16 +02:00
# for exact matches
if pixel == white:
pixel_bin_rep = "1"
2020-10-05 12:33:16 +02:00
elif pixel == black:
pixel_bin_rep = "0"
2020-10-05 12:33:16 +02:00
else:
white_diff = tuple(map(abs, map(sub, white, pixel)))
2020-10-05 12:33:16 +02:00
# min_diff = white_diff
black_diff = tuple(map(abs, map(sub, black, pixel)))
2020-10-05 12:33:16 +02:00
# if the white difference is smaller, that means the pixel is closer
# to white, otherwise, the pixel must be black
if all(map(less, white_diff, black_diff)):
pixel_bin_rep = "1"
2020-10-05 12:33:16 +02:00
else:
pixel_bin_rep = "0"
2020-10-05 12:33:16 +02:00
# adding bits
bits += pixel_bin_rep
2020-10-05 12:33:16 +02:00
return (bits, done)
def get_bits_from_video(video_filepath):
# get image sequence from video
print('Reading video...')
2020-10-05 12:33:16 +02:00
image_sequence = []
os.system('ffmpeg -i ' + video_filepath + ' ./fvid_frames/decoded_frames_%d.png');
2020-10-05 12:33:16 +02:00
# for filename in glob.glob(f"{FRAMES_DIR}decoded_frames*.png"):
for filename in sorted(glob.glob(f"{FRAMES_DIR}decoded_frames*.png"), key=os.path.getmtime) :
2020-10-05 12:33:16 +02:00
image_sequence.append(Image.open(filename))
bits = ""
sequence_length = len(image_sequence)
print('Bits are in place')
for index in tqdm(range(sequence_length)):
2020-10-05 12:33:16 +02:00
b, done = get_bits_from_image(image_sequence[index])
bits += b
if done:
break
return bits
def save_bits_to_file(file_path, bits, key):
2020-10-05 12:33:16 +02:00
# get file extension
bitstring = Bits(bin=bits)
#zip
print('Unziping...')
in_ = io.BytesIO()
in_.write(bitstring.bytes)
in_.seek(0)
with gzip.GzipFile(fileobj=in_, mode='rb') as fo:
bitstring = fo.read()
#zip
unpickled = pickle.loads(bitstring)
tag = unpickled['tag']
ciphertext = unpickled['data']
filename = unpickled['filename']
cipher = AES.new(key, AES.MODE_EAX, nonce=SALT)
bitstring = cipher.decrypt(ciphertext)
print('Checking integrity...')
try:
cipher.verify(tag)
except ValueError:
raise WrongPassword("Key incorrect or message corrupted")
bitstring = BitArray(bitstring)
2020-10-05 12:33:16 +02:00
# If filepath not passed in use defualt
# otherwise used passed in filepath
if file_path == None:
filepath = filename
else:
filepath = file_path # No need for mime Magic
2020-10-05 12:33:16 +02:00
with open(
filepath, "wb"
2020-10-05 12:33:16 +02:00
) as f:
bitstring.tofile(f)
def split_list_by_n(lst, n):
for i in range(0, len(lst), n):
yield lst[i : i + n]
def make_image_sequence(bitstring, resolution=(1920, 1080)):
width, height = resolution
# split bits into sets of width*height to make (1) image
set_size = width * height
# bit_sequence = []
print('Making image sequence')
print('Cutting...')
#bitlist = list(tqdm(split_list_by_n(bitstring, set_size)))
bitlist = list(split_list_by_n(bitstring, set_size))
del bitstring
bitlist[-1] = bitlist[-1] + '0'*(set_size - len(bitlist[-1]))
2020-10-05 12:33:16 +02:00
index = 1
bitlist = bitlist[::-1]
print('Saving frames...')
for _ in tqdm(range(len(bitlist))):
bitl = bitlist.pop()
# for bitl in tqdm(bitlist):
# image_bits = list(map(int, tqdm(bitl)))
image_bits = list(map(int, bitl))
# print(image_bits)
2020-10-05 12:33:16 +02:00
image = Image.new("1", (width, height))
image.putdata(image_bits)
image.save(
f"{FRAMES_DIR}encoded_frames_{index}.png"
)
index += 1
2020-10-05 12:33:16 +02:00
def make_video(output_filepath, framerate="1/5"):
2020-10-05 12:33:16 +02:00
if output_filepath == None:
outputfile = "file.mp4"
else:
outputfile = output_filepath
os.system('ffmpeg -r ' + framerate + ' -i ./fvid_frames/encoded_frames_%d.png -c:v libx264rgb ' + outputfile)
2020-10-05 12:33:16 +02:00
def cleanup():
# remove frames
import shutil
shutil.rmtree(FRAMES_DIR)
def setup():
import os
if not os.path.exists(FRAMES_DIR):
os.makedirs(FRAMES_DIR)
2020-10-07 16:14:32 +02:00
def main():
2020-10-05 12:33:16 +02:00
parser = argparse.ArgumentParser(description="save files as videos")
parser.add_argument(
"-e", "--encode", help="encode file as video", action="store_true"
)
parser.add_argument(
"-d", "--decode", help="decode file from video", action="store_true"
)
parser.add_argument("-i", "--input", help="input file", required=True)
parser.add_argument("-o", "--output", help="output path")
2020-10-08 15:38:31 +02:00
parser.add_argument("-f", "--framerate", help="set framerate for encoding (as a fraction)", default="1/5", type=str)
parser.add_argument("-p", "--password", help="set password", nargs="?", type=str, default='default')
2020-10-05 12:33:16 +02:00
args = parser.parse_args()
setup()
if not NOTDEBUG:
print('args', args)
print('PASSWORD', args.password, [len(args.password) if len(args.password) is not None else None for _ in range(0)])
if not args.decode and not args.encode:
raise MissingArgument('You should use either --encode or --decode!') #check for arguments
key = get_password(args.password)
2020-10-05 12:33:16 +02:00
if args.decode:
bits = get_bits_from_video(args.input)
file_path = None
2020-10-05 12:33:16 +02:00
if args.output:
file_path = args.output
save_bits_to_file(file_path, bits, key)
2020-10-05 12:33:16 +02:00
elif args.encode:
2020-10-07 20:19:58 +02:00
# isdigit has the benefit of being True and raising an error if the user passes a negative string
# all() lets us check if both the negative sign and forward slash are in the string, to prevent negative fractions
if (not args.framerate.isdigit() and "/" not in args.framerate) or all(x in args.framerate for x in ("-", "/")):
raise NotImplementedError("The framerate must be a positive fraction or an integer for now, like 3, '1/3', or '1/5'!")
2020-10-05 12:33:16 +02:00
# get bits from file
bits = get_bits_from_file(args.input, key)
2020-10-05 12:33:16 +02:00
# create image sequence
make_image_sequence(bits)
2020-10-05 12:33:16 +02:00
video_file_path = None
2020-10-05 12:33:16 +02:00
if args.output:
video_file_path = args.output
make_video(video_file_path, args.framerate)
2020-10-05 12:33:16 +02:00
cleanup()