-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathVideoification.py
More file actions
344 lines (306 loc) · 15.1 KB
/
Copy pathVideoification.py
File metadata and controls
344 lines (306 loc) · 15.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
from segno import make, make_sequence , encoder, DataOverflowError
import ffmpeg as mv
from sopsy import Sops, SopsyInOutType
from pathlib import Path, WindowsPath, PosixPath
import Constants
import pyzbar.pyzbar as zbar
import struct
import zlib
class Videoification():
def __init__(self, file_path, output_path, verbose=Constants.DEBUG_MODE, initial_data_buffer=1, frame_buffer=3, qr_version=1, split_size_override=None, output_resolution="480P", parallel_processing=False):
if not Path(file_path).exists():
raise FileNotFoundError(f"File '{file_path}' does not exist")
if qr_version > 40 or qr_version < 1:
raise ValueError("QR Code Level must be between 1 and 40")
if split_size_override is not None and split_size_override > 40000:
raise DataOverflowError("Split size cannot exceed 40KB (the maximum size of a QR code)")
self.file_path = Path(file_path)
self.output_path = Path(output_path)
self._processing_folder = Path("C://Users/jediw/Documents/GitHub/Infinite-Cloud-Storage-Solution/downloaded_files/" + self.output_path.name)
self._processing_folder.mkdir(exist_ok=True)
self.frame_buffer = frame_buffer
self.initial_data_buffer = initial_data_buffer
self.qr_version = qr_version
self._split_size = split_size_override or 40000
self._output_resolution = Constants.VIDEO_RESOLUTIONS[output_resolution]
self._FRAME_RATE = Constants.FRAME_RATE
self.sops = Sops(self.file_path, output_type=SopsyInOutType.BINARY)
self.ffmpeg_bin = Constants.FFMPEG_PATH
self.ffprobe_bin = Constants.FFPROBE_PATH
self.verbose = verbose
self.parallel_processing = parallel_processing
self.probe_results = None
def encrypt_file(self):
self.sops.encrypt()
def decrypt_file(self):
self.sops.decrypt()
def compile_video(self, qr_codes=None):
# Basically a list of QR Code Image files
qr_codes = qr_codes or [Path(str(qr_code)) for qr_code in Path(str(self._processing_folder) + "/QR Codes").iterdir()]
# Create video clips for each QR Code
concat_file = Path(str(self._processing_folder) + "/concat.txt")
concat_text = ''
# Creates a buffer at the beginning of the video for {intial_data_buffer} frames
blank_clip = mv.input(
f"color=black:s={Constants.QR_CODE_IMAGE_SIZE}x{Constants.QR_CODE_IMAGE_SIZE}", # Tells it what color I want and keeps video sizing consistent
f="lavfi", # Tells it to generate a solid color frame
r=self._FRAME_RATE, # Sets video clip Frame Rate
t=self.initial_data_buffer / self._FRAME_RATE # Sets duration for {initial_data_buffer} frames
)
if self.verbose is True:
print(f"Processed blank space buffer (duration: {self.initial_data_buffer}, type: {type(blank_clip)})")
for qr_code in qr_codes:
concat_text += f"file '{qr_code}'\nduration {self.frame_buffer / self._FRAME_RATE}\n"
concat_text += f"file '{qr_codes[0]}'\nduration 0\n"
concat_file.write_text(concat_text)
if self.verbose is True:
print(f"Processed QR Codes (duration: {self.frame_buffer}, Total QR Codes: {len(qr_codes)}, Projected Duration: {len(qr_codes) * self.frame_buffer + self.initial_data_buffer} frames)")
# Concatenate all video clips into one and sets up the finished video's formatting
all_qr_codes = mv.input(concat_file, format="concat", safe=0)
video = mv.concat(blank_clip, all_qr_codes, unsafe=False) # Concatinates the color clip and all QR Codes together
video = video.output(
str(self.output_path), # Outputs the finished video at the given file location
vcodec="libx264", # Sets the finished video's codec
framerate=self._FRAME_RATE, # Sets the finished video's frame rate
s=self._output_resolution, # Sets the finished video's resolution (default is 480P)
pix_fmt="yuv420p", # Sets the finished video's pixel format
)
video = video.overwrite_output() # Automatically overwrites if there's something saved where
# With all the instructions set, it passes them to FFMPEG and it does what I want it to
video = video.run(
cmd=self.ffmpeg_bin, # Where I have FFMPEG saved on my system
capture_stdout=self.verbose # Makes FFMPEG's Verbose terminal visible for debugging
)
return video
def decompile_video(self):
Path(str(self._processing_folder) + "/QR Codes").mkdir(exist_ok=True)
# Gets rid of frame buffer
self.probe_results = mv.probe(str(self.file_path), cmd=self.ffprobe_bin)
self._FRAME_RATE = eval(self.probe_results['streams'][0]['r_frame_rate'])
total_frames = int(self.probe_results['streams'][0]['nb_frames'])
video = mv.input(str(self.file_path), r=self._FRAME_RATE)
video = video.filter("scale", Constants.QR_CODE_IMAGE_SIZE, Constants.QR_CODE_IMAGE_SIZE)
video = video.filter("select", f"between(n, {self.initial_data_buffer}, {total_frames})*not(mod(n, {self.frame_buffer}))")
video = video.setpts("N/FRAME_RATE/TB")
video = video.output(f"{self._processing_folder}/QR Codes/{self.output_path.stem}_Decomp_Frame%d.png", f="image2", pix_fmt="yuv420p")
video.run(cmd=self.ffmpeg_bin, capture_stdout=True)
def extract_qr_code(self, qr_code: Path | bytes):
if isinstance(qr_code, Path) or isinstance(qr_code, WindowsPath) or isinstance(qr_code, PosixPath):
qr_code = qr_code.read_bytes()
decompiled_png = self.read_png(qr_code)
decoded_qr_code = zbar.decode((decompiled_png, Constants.QR_CODE_IMAGE_SIZE, Constants.QR_CODE_IMAGE_SIZE))
if qr_code is not None:
return decoded_qr_code[0].data
# raise NotImplementedError("I uhh... didn't do this yet")
def format_png(self, png: bytes) -> list[tuple[str, bytes]]:
# Verify the PNG signature
if png[:8] != b"\x89PNG\r\n\x1a\n":
if self.verbose is True:
print(f"Passed var {type(png)} is not a PNG file!")
raise ValueError("Passed var is not a PNG file!")
return None
png = png[8:]
formatted_png_chunks = []
chunk_number = 0
while png:
# Loops through every chunk in the PNG file
# PNG Chunk Structure:
# 0-4 bytes: Length of the actual PNG data contained in the chunk
# 4-8 bytes: Gets the tyoe of Chunk type
# IDAT: We are reading somewhere in the middle of the PNG file
# IEND: We are at the last chunk of the PNG file
# 8- (8 + Chunk Length) bytes: The chunk's stored PNG data
# (8 + Chunk Length) - (8 + Chunk Length + 4) bytes: data verification info ( via a Cyclic Redundancy Check)
# Iterate the current chunk number
chunk_number += 1
# Read the chunk length (first 4 bytes)
# a PNG's compressed bits are ordered from greatest to least (>)
# and the length is an unsigned integer (I)
length = struct.unpack(">I", png[:4])[0]
# Moves from chunk length to the chunk type
png = png[4:]
chunk_type = png[:4].decode("ascii")
# Moves from the chunk type to the actual chunk data
png = png[4:]
chunk_data = png[:length]
# Moves from the chunk data to the CRC data
png = png[length:]
# Read the stored CRC (Cyclic Redundancy Check) and verifies our extracted data is correct
stored_crc = struct.unpack(">I", png[:4])[0]
calculated_crc = zlib.crc32(chunk_type.encode("ascii"))
calculated_crc = zlib.crc32(chunk_data, calculated_crc)
if stored_crc != calculated_crc:
if self.verbose is True:
print(f"CRC Mismatch at chunk {chunk_number}: {stored_crc} != {calculated_crc}")
raise ValueError(f"CRC Mismatch at chunk {chunk_number}: {stored_crc} != {calculated_crc}")
# If it's the IDAT chunk, append the data to the decompressed PNG
# if chunk_type == "IDAT":
formatted_png_chunks.append((chunk_type, chunk_data))
# If it's the IEND chunk, stop reading because nothing's there
if chunk_type == "END":
break
# Move from CRC data to the start of the next chunk
png = png[4:]
return formatted_png_chunks
def filter_png(self, formatted_png_chunks: list[tuple[str, bytes]]):
"""
How I think the PNG file is structured:
- PNG Signature (8 bytes; Handled by format_png)
- PNG Chunk 1 (IHDR)
- 0-4 bytes: Image Width. Gives the image dimensions in pixels
- 4-8 bytes: Image Height. Gives the image dimensions in pixels
- 8-9 bytes: Bit Depth. Gives the number of bits per sample or per palette index (not per pixel). Accepted Values:.
- 9-10 bytes: Color type. Defines the PNG image type. Accepted values:
- 0 (grayscale)
- 2 (truecolor)
- 3 (indexed-color)
- 4 (greyscale with alpha)
- 6 (truecolor with alpha)
- 10-11 bytes: Compression method. Indicates the method used to compress the image data. Only compression method 0 (deflate/inflate compression with a sliding window of at most 32768 bytes) is defined in the spec.
- 11-12 bytes: Filter method. Indicates the preprocessing method applied to the image data before compression. Only filter method 0 (adaptive filtering with five basic filter types) is defined in the spec.
- 12-13 bytes: Interlace method. Indicates whether there is interlacing. Two values are defined in the spec: 0 (no interlace) or 1 (Adam7 interlace).
- PNG Chunk 2-X (IDAT)
- PNG Chunk X+1 (IEND)
"""
# Decompress the PNG data
decompressed_png = b''
# If the PNG file is not formatted correctly, return None
if formatted_png_chunks is None:
if self.verbose is True:
print(f"[ERROR] PNG file failed during formatting")
return None
if formatted_png_chunks[0][0] != "IHDR":
if self.verbose is True:
print(f"[ERROR] First chunk is not IHDR")
raise ValueError("First chunk is not IHDR")
if formatted_png_chunks[-1][0] != "IEND":
if self.vehrbose is True:
print(f"[ERROR] Last chunk is not IEND")
raise ValueError("Last chunk is not IEND")
png_data = {}
for chunk_type, chunk_data in formatted_png_chunks:
if chunk_type == "IHDR":
# Read the image width, height, bit depth, color type, compression method, filter method, and interlace method
png_data["Width"] = struct.unpack(">I", chunk_data[:4])[0]
png_data["Height"] = struct.unpack(">I", chunk_data[4:8])[0]
png_data["Depth"] = struct.unpack("B", chunk_data[8:9])[0]
png_data["Color"] = struct.unpack("B", chunk_data[9:10])[0]
png_data["Compression"] = struct.unpack("B", chunk_data[10:11])[0]
png_data["Filter"] = struct.unpack("B", chunk_data[11:12])[0]
png_data["Interlace"] = struct.unpack("B", chunk_data[12:13])[0]
elif chunk_type == "IDAT":
decompressed_png += chunk_data
elif chunk_type == "IEND":
break
else:
# if self.verbose is True:
print(f"[WARNING] Unknown PNG chunk type: {chunk_type}")
# decompressed_png = zlib.decompress(decompressed_png)
# Calculate the number of bytes per pixel
bytes_per_pixel = Constants.PNG_TYPE_TO_PIXEL_SIZE[png_data["Color"]] * png_data["Depth"] // 8
if self.verbose is True:
print(f"Decompressed PNG Length: {len(decompressed_png)} bytes")
print(f"Image Width: {png_data["Width"]} | Image Height: {png_data["Height"]} | Bit Depth: {png_data["Depth"]} | BPP: {bytes_per_pixel} | Color Type: {png_data["Color"]} | Compression Method: {png_data["Compression"]} | Filter Method: {png_data["Filter"]} | Interlace Method: {png_data["Interlace"]}")
if bytes_per_pixel != 8:
if self.verbose is True:
print(f"[ERROR] Invalid bytes per pixel: {bytes_per_pixel} (Expected 8)")
raise ValueError(f"Invalid bytes per pixel: {bytes_per_pixel} (Expected 8)")
# if len(de640a2708-e230-4044-8a09-5e90b5441012compressed_png) % (image_width * image_height * bytes_per_pixel + 1) != 0:
# if self.verbose is True:
# print(f"[ERROR] Decompressed PNG data is not a multiple of the expected size ({image_width} * {image_height} * {bytes_per_pixel} + 1 != % {len(decompressed_png)})")
# print(f"Decompressed PNG Length: {len(decompressed_png)} bytes")
# print(f"Image Width: {image_width} | Image Height: {image_height} | Bit Depth: {bit_depth} | BPP: {bytes_per_pixel} | Color Type: {color_type} | Compression Method: {compression_method} | Filter Method: {filter_method} | Interlace Method: {interlace_method}")
# raise ValueError("Decompressed PNG data is not a multiple of the expected size. Is the BPP correct?")
return png_data, decompressed_png
def read_png(self, png: bytes):
# # Format the PNG file
# formatted_png_chunks = self.format_png(png)
# # Filter the PNG file
# png_data, combined_png_chunks = self.filter_png(formatted_png_chunks)
return combined_png_chunks
def extract_qr_codes(self, qr_codes=None):
qr_codes: list = qr_codes or [qr_code for qr_code in Path(str(self._processing_folder) + "/QR Codes").iterdir()]
return [self.extract_qr_code(qr_code) for qr_code in qr_codes]
def generate_qr_code(self, qr, file_name):
# qr = make(data,
# version=self.qr_version,
# error='l',
# mode='byte'
# )
return qr.save(file_name + ".png",
scale=1,
border=4,
kind="png",
)
def generate_qr_codes(self, data):
# qr_codes = []
# for i, data in enumerate(data):
# qr_code = self.generate_qr_code(data, f"{i}")
# qr_codes.append(qr_code)
Path(str(self._processing_folder) + "/QR Codes").mkdir(exist_ok=True)
qr_codes = make_sequence(data, 'l', 40, "byte", parallel_process=self.parallel_processing) # type: ignore
# qr_codes.save(str(self._processing_folder) + "/QR Codes/%d.png", scale=1, border=4, kind="png")
for i, qr_code in enumerate(qr_codes):
self.generate_qr_code(qr_code, str(self._processing_folder) + f"/QR Codes/{i}")
def split_file(self):
# with open(self.file_path, "rb") as file:
# data = file.read()
# file_size = len(data)
# split_count = file_size // self._split_size
# if file_size % self._split_size != 0:
# split_count += 1
# print(f"Splitting file into {split_count} {self._split_size}-byte-large parts")
# return [data[i * self._split_size:(i + 1) * self._split_size] for i in range(int(split_count))]
return self.file_path.read_bytes()
def merge_file(self, file_parts:list):
with open(self.output_path, "wb") as file:
for part in file_parts:
file.write(part)
return file
def cleanup(self, folder_path=None, delete_self=False):
folder_path = folder_path or self._processing_folder
folder_path = Path(folder_path)
if folder_path.is_dir():
for file in folder_path.iterdir():
try:
file.unlink()
except PermissionError:
pass
if file.is_dir():
self.cleanup(file)
try:
folder_path.rmdir()
except PermissionError:
pass
else:
try:
folder_path.unlink()
except PermissionError:
pass
if delete_self:
del self
def transform(self):
import time
# self.encrypt_file()
split_data = self.split_file()
if self.verbose is True:
print(f"Creating QR Codes...")
start_time = time.time()
self.generate_qr_codes(split_data)
print(f"QR Codes Created | time: {time.time() - start_time} sec")
self.compile_video()
# self.cleanup()
return self.output_path
def reverse_transform(self):
self.decompile_video()
split_data = self.extract_qr_codes()
self.merge_file(split_data)
# self.decrypt_file()
# self.cleanup()
return self.output_path
if __name__ == "__main__":
# video = Videoification("mhm.md", "test.mp4", frame_buffer=100, initial_data_buffer=10, parallel_processing=True)
# compiled_video = video.transform()
# print(f"{compiled_video} has been created! Now to reverse the process...")
compiled_video = Videoification("test.mp4", "converted.md", frame_buffer=100, initial_data_buffer=10, parallel_processing=False)
compiled_video.reverse_transform()