Repository: MarkMichon1/BitGlitter-Python (language: python)

Issue:
Replaced code in coloranalysis.py with function call to return_distance
In coloranalysis.py 
Function `return_distance` calculates the Euclidean Distance between two points
```
def return_distance(raw_frame_rgb, expected_value):
    return math.sqrt(((raw_frame_rgb[0] - expected_value[0]) ** 2) + ((raw_frame_rgb[1] - expected_value[1]) ** 2) +
                     ((raw_frame_rgb[2] - expected_value[2]) ** 2))
```
Function `color_snap` also manually calculates the Euclidean Distance
```
        active_distance = math.sqrt(((raw_frame_rgb[0] - color[0]) ** 2) + ((raw_frame_rgb[1] - color[1]) ** 2)
                                   + ((raw_frame_rgb[2] - color[2]) ** 2))
```
I simply replaced the code here with a function call to `return_distance`
```        active_distance = return_distance(raw_frame_rgb, color)```
Error when installing with the local setup.py in a virtual environment.
After cloning the repository and attempting to install it in a virtual environment by running the following command.

```
$ pipenv run pip3 install -e .
```

The following error is raised.
```
Obtaining file:///home/dgnsrekt/dev/python/BitGlitter
    ERROR: Command errored out with exit status 1:
     command: /home/dgnsrekt/.local/share/virtualenvs/BitGlitter-h2UJ9Cja/bin/python3.7m -c 'import sys, setuptools, tokenize; sys.argv[0] = '"'"'/home/dgnsrekt/dev/python/BitGlitter/setup.py'"'"'; __file__='"'"'/home/dgnsrekt/dev/python/BitGlitter/setup.py'"'"';f=getattr(tokenize, '"'"'open'"'"', open)(__file__);code=f.read().replace('"'"'\r\n'"'"', '"'"'\n'"'"');f.close();exec(compile(code, __file__, '"'"'exec'"'"'))' egg_info
         cwd: /home/dgnsrekt/dev/python/BitGlitter/
    Complete output (1 lines):
    error in BitGlitter setup command: ('Invalid group name', 'console scripts')
    ----------------------------------------
ERROR: Command errored out with exit status 1: python setup.py egg_info Check the logs for full command output.
```


merge staging to master



Discussion / hints:

Fix patch:
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
deleted file mode 100644
index d89c77c..0000000
--- a/CODE_OF_CONDUCT.md
+++ /dev/null
@@ -1,1 +0,0 @@
-Act like an adult.
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ff95e3f..b771728 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,13 +1,13 @@
 # How to contribute
 
-Hey, thanks for your interest in BitGlitter.  Our current build is the just the beginning of what we want to accomplish.
+Thanks for your interest in BitGlitter.  Our current build is the just the beginning of what we want to accomplish.
   If you find this project as exciting as we do and wish to contribute, you can play a major role in the direction and
   evolution of what BitGlitter can become.  Visit the below links to learn more!
   
 Here's a few important resources:
-+ [BitGlitter Wikipedia](https://github.com/MarkMichon1/BitGlitter/wiki) - A continually growing resource for this 
-project.  Includes guides, roadmaps, and more.
-+ [Discord Channel](TBA) - This is the nerve center of BitGlitter.  It's highly recommended you 
++ [BitGlitter Wikipedia](https://github.com/MarkMichon1/BitGlitter-Python/wiki) - A continually growing resource for this 
+project.  Includes guides, roadmaps, and more. *(outdated as of 2021)*
++ [Discord Channel](https://discord.gg/t9uv2pZ) - This is the nerve center of BitGlitter.  It's highly recommended you 
 join if you're interested in contributing!
 
 # Coding conventions
@@ -17,7 +17,7 @@ Check out our current code in ```master``` to get a feel for what we're looking
  + Use spacing to divide different logical parts of functions or behavior.  This greatly helps with readability!
  + For any sufficiently complicated piece of code, please include dosctrings inside of the object, to better explain
  what it does.
- + camelCase is what's used for naming variables and functions.
+ + standard_python_naming is what's used for naming variables and functions.
  
 More may be added if anything arises.  Feel free to make a suggestion if you think I'm missing anything important! 
 Thanks!
diff --git a/LICENSE b/LICENSE
index ed965ed..31a5ba3 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
 MIT License
 
-Copyright (c) 2019 - ∞ Mark Michon
+Copyright (c) 2021 - ∞ Mark Michon
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 8f32b43..5f265a2 100644
--- a/README.md
+++ b/README.md
@@ -1,416 +1,498 @@
-[![Downloads](https://pepy.tech/badge/bitglitter)](https://pepy.tech/project/bitglitter)
-
-**[Discord Server](https://discord.gg/t9uv2pZ)**
-
-![BitGlitter Logo](https://i.imgur.com/3GJ4bDx.png)
+![BitGlitter Logo](https://i.imgur.com/pX8b4Dy.png)
 
-# The basics
+Latest: `v2.0.0`
 
-![BitGlitter Sample GIF](https://i.imgur.com/n7E7lnd.gif)
+### Python Library (you are here) | [Electron Desktop App](https://github.com/MarkMichon1/BitGlitter) | [Python Backend For App](https://github.com/MarkMichon1/BitGlitter-Backend)
 
-**[Click here](https://www.youtube.com/watch?v=HrY4deFrOoA) for a demo video of a real stream.**
+## ⚡ Store and transfer files using high-performance animated barcodes
 
-BitGlitter is an easy to use library that allows you to embed data inside of ordinary pictures or video.  Store and host
-files wherever images or videos can be hosted.
+![BitGlitter Sample GIF](https://i.imgur.com/lPFR5kA.gif) 
 
-### From physical barcodes to digital data transfer
+**[Discord Server](https://discord.gg/t9uv2pZ)** 
+[![Downloads](https://pepy.tech/badge/bitglitter)](https://pepy.tech/project/bitglitter)
 
-Whether it's barcodes at the store or QR codes you can scan with your phone- they both work on the same principle.  Data
-is encoded into the black and white.  You can think of each color as an abstraction for a binary value, so then when
-those colors are read in sequence, you can pull meaningful data from the image.  I wondered how this concept could be
-improved upon, and I wanted a cool first project as an introduction to programming.  BitGlitter was born.
+**[Youtube video of a real stream transferring ~80KB/s of data](https://youtu.be/TIKEEA2mXrI)**
+
+BitGlitter is an easy to use Python library that lets you embed data inside ordinary pictures or video.  Store and host
+files wherever images or videos can be hosted.  The carrier for data is the 'blocks' within the frames and not
+the file itself, and there are various measures to read imperfect distorted frames.  What this means for you is streams
+are resistant to compression and distortion, and aren't broken by things such as format changes, metadata changes, etc.
+BitGlitter gives you a unique way to make your data more portable.
+
+![Frame Demo](https://i.imgur.com/Pgq4h1o.png)
+
+Sample frame taken from video using default settings, holding 2.7 KB of data
+
+### Using ordinary barcodes as a launchpad
+
+Barcodes and QR codes are everywhere.  They embed binary data (0's and 1's) in them, symbolized as black and white.  While
+they are pretty constrained in the real world, using them for digital transfer removes many of those limits.  What if you
+could have multiple barcodes (frames), that if read sequentially could have the capacity of many thousands of individual
+ones?  What if we added colors to the barcodes, so a given barcode could have 2x, 6x, 24x the capacity?  What if we greatly
+increased the size of the frames to lets say the size of a standard 1080p video, so the frames once again increase their
+capacity by a couple orders of magnitude.  Combine all of these together, and you're able to move serious amounts of 
+data.  This is BitGlitter.
+
+![BitGlitter Default Palettes](https://i.imgur.com/T6CiFOf.png)
+
++ **Designed to survive what breaks existing steganography schemes:** BitGlitter doesn't rely on reading the exact stream that
+it outputs at write.  Because the data is in the video data itself and not in any metadata or byte format embedded in it,
+its resistant to format changes, resolution changes, compression, corruption, and other distortion.  Not much different
+from how barcodes in real life are resistant.
++ **Hardened against frame corruption:** Virtually all video/image hosting social media type sites run your multimedia 
+files through compression to minimize their file size.  This can cause compression artifacts (visual distortions) to 
+appear.  In lossless steganography, this will completely corrupt the data, rendering it unreadable.  Not BitGlitter.  
+Taking in the palette used in the stream, it will "snap" incorrect colors to their nearest value, allowing you to read 
+data from frames that have gone though a gauntlet of compression.  The default write settings have been tested on 
+several major sites, and were tweaked until there was 100% readability across tens of thousands of test frames.
++ **Fully configurable stream creation:** While the default values cover most uses, you have full control of the write parameters:
+  + **Color set:** 8 default palettes to choose from that provide higher performance or greater file integrity.  You can
+  even make your own custom palettes (more on that below)
+  + **Block size:** These are the colorful squares that hold data.  They can be as small as one pixel, or as large as
+  you'd like.  Larger block sizes gives you greater data integrity, and smaller block sizes increase data capacity per
+  frame.
+  + **Frame dimensions:** Whether you want to output 144p compatible videos or 8K, it does it all.
+  + **Frame rate:** If its a non-zero whole number, yep
+  + **Output mode:**  Choose from either an MP4 video output, or a series of PNG images (BitGlitter accepts and reads both),
+  giving you greater flexibility on where you can host your data.
++ **Built in file integrity:** Metadata, files, and the stream and frames themselves are protected with SHA-256 hashes.
+Only valid data is accepted.
++ **Built in encryption and file masking:** Encrypt your files with AES-256, and optionally the file manifest as well, 
+masking its contents until the correct key is used.
++ **Built in compression:** Payloads are compressed using max zlib settings prior to rendering, to minimize stream 
+size.  No need to zip or rar your files prior.
++ **Supports very large streams:** Current protocol can handle up to one [exabyte](https://en.wikipedia.org/wiki/Byte#Multiple-byte_units)
+in size, or ~4.3 billion rendered frames.  Put simply, there's no practical limit to your stream's size.
+
+![Custom Color Showcase](https://i.imgur.com/o4xa0Fq.png)
+
+### What is possible with various configurations?
+
+Because everything can be customized, you can have completely different outputs with vastly different performance
+characteristics (data integrity vs performance).  In **bold** is the default parameters used which have been 
+consistently readable on major social media sites.  This is all new territory, what is readable can likely be tweaked and increased even more.
+
+| Number of Colors in Palette 	| Bits of Data Per Block 	| Frame Resolution        	| Block Size in Pixels 	| Frame Dimensions (w * h in blocks) 	| Frames Rate 	| Data Throughput 	| Lossless? 	|
+|:---------------------------:	|------------------------	|-------------------------	|----------------------	|------------------------------------	|-------------	|-----------------	|-----------	|
+| 2                           	| 1                      	| 640 x 480 (480p)        	| 20                   	| 32 x 24                            	| 30 FPS      	| 1.56 KB/s       	| No        	|
+| 4                           	| 2                      	| 1280 x 720 (720p)       	| 20                   	| 64 x 36                            	| 30 FPS      	| 15.96 KB/s      	| No        	|
+| 8                           	| 3                      	| 1280 x 720 (720p)       	| 20                   	| 64 x 36                            	| 30 FPS      	| 24.6 KB/s       	| No        	|
+| 16                          	| 4                      	| 1920 x 1080 (1080p)     	| 24                   	| 80 x 45                            	| 30 FPS      	| 52.68 KB/s      	| No        	|
+| **64**                      	| **6**                  	| **1920 x 1080 (1080p)** 	| **24**               	| **80 x 45**                        	| **30 FPS**  	| **79.68 KB/s**  	| **No**    	|
+| 64                          	| 6                      	| 1920 x 1080 (1080p)     	| 24                   	| 80 x 45                            	| 60 FPS      	| 159.36 KB/s     	| No        	|
+| 16,777,216                  	| 24                     	| 1920 x 1080 (1080p)     	| 5                    	| 384 x 216                          	| 30 FPS      	| 7.46 MB/s       	| Yes       	|
+| 16,777,216                  	| 24                     	| 3840 x 2160 (4K)        	| 5                    	| 768 x 432                          	| 60 FPS      	| 59.71 MB/s      	| Yes       	|
 
-Conventional barcodes are severely limited in application, in terms of their data density.  When you maximize the 
-concept and configure it for digital-digital transmission, a lot of capability is gained.  
-BitGlitter is in a class of it's own in several ways:
+### Installation
 
-![BitGlitter Default Palettes](https://i.imgur.com/dSYmq7V.png)
+In addition to downloading the code from Github, you can also grab it directly from PyPI:
 
-+ **Multiple Color Palettes:**  By removing the constraint of only using black and white, the amount of data you can 
-hold in a given "block" (each square) on the frame skyrockets.  The regular two color setup holds one bit per block.  
-Four colors holds two  bits (2x), sixty four colors holds six bits (6x), and lossless ~16.7M color palette holds 24 bits 
-(24x improvement over black & white).  You choose what color palette you'd like to use according to your application.  
-Smaller sets are far resistant to compression and corruption, while larger sets have higher data densities.  You can
-create your own custom palettes as well with whatever colors you'd like to use.  More about that below.
-+ **Multi-Frame Videos:** BitGlitter automatically breaks up larger files into multiple frames, with several headers
-built into them.  These include several layers of protection against corruption, as well as metadata about the frame
-as well as the stream itself so the reader can intelligently decide how to handle it.  By stitching these frames
- together and turning them into a video, you can embed files and folders of arbitrary size into videos and images.  
-You're only limited by your hard drive.
-+ **Variable block size:** Each of the blocks in the frame can be set to any size, including one pixel.  Larger block 
-sizes give your stream protection in lossy environments, while smaller blocks allow for greater densities.
+`pip install bitglitter`
 
-Currently, BitGlitter is configured to transport files and folders on a computer.  But with minimal modification you can
-use videos or images as a carrier for virtually any kind of binary data.
+###**The 2 Core Functions of BitGlitter**
 
-### What this means in practical terms
+Ignoring the bells and whistles for now, all you need to use BitGlitter is `write()` and `read()`.  Both use an assortment
+of default arguments to remove a lot of the complexity starting out.
 
-Here's some real data that gives you an idea of what is possible with this:
++ `write()` takes your files and directories, and creates the BitGlitter stream (either as a video of a collection of 
+images).
++ `read()` scans your BitGlitter encoded files and outputs the files/directories embedded in it.
 
-Number of Colors | Bits Per Block | Screen Resolution | Block Size in Pixels | Block Dimensions | Framerate | Throughput | Lossless Application
---- | --- | --- | --- | --- | --- | --- | ---
-2 | 1 | 640 x 480 (480p) | 20 | 32 x 24 | 30 | 2.88 KB/s | No
-4 | 2 | 1280 × 720 (720p) | 20 | 64 x 36 | 30 | 17.28 KB/s | No
-8 | 3 | 1280 × 720 (720p) | 20 | 64 x 36 | 30 | 25.92 KB/s | No
-16 | 4 | 1920 x 1080 (1080p) | 20 | 96 x 54 | 30 | 77.76 KB/s | No
-64 | 6 | 1920 x 1080 (1080p) | 20 | 96 x 54 | 30 | 116.64 KB/s | No
-64 | 6 | 1920 x 1080 (1080p) | 20 | 96 x 54 | 60 | 233.28 KB/s | No
-16,777,216 | 24 | 1920 x 1080 (1080p) | 5 | 384 x 216 | 30 | 7.47 MB/s | Yes
-16,777,216 | 24 | 3840 x 2160 (4k) | 5 | 768 x 432 | 60 | 59.7 MB/s | Yes
+### write() -- converting files and directories into BitGlitter streams
 
-Put simply, you can now make videos that can hold large amounts of data inside of them.  There may be some pretty
-interesting applications that can come out of this.
+We'll go a bit more in depth now.
 
-# Features
+`write()` is the function that inputs files and turns them into a BitGlitter stream.  There are quite a few arguments
+to customize the stream, but there is only one required argument.  Everything else has defaults.
 
-### Data
+`input_path` is an absolute path pointing to the file or directory you'd like convert into a stream.
 
-+ **Supports streams up to ~1 EB in size, or ~4.3B frames:**  In other words, there is no practical limit to the
- stream's size.
-+ **Compression added:** This is done automatically, so don't worry about putting your files in a rar or zip prior to
-sending.
-+ **Encryption added:** Optional AES-256 encryption to protect your files.  Passwords are hashed with `scrypt`, 
-parameters can be customized for your needs.
-+ **File masking:**  Optional ability to mask what files are included in the stream.  Only those who successfully grab 
-the stream (and decrypt it if applicable) will know of its contents.
+`preset_nickname=None` takes in a string name for a preset.  Learn more below in **Preset Functions**.
 
-### Outputted Files
+`stream_name=''` **required** argument to name your stream, which will be encoded into the metadata of the stream when
+read.  150 character limit.
 
-You can choose between either outputting all of your frames as a series of images (.png), or as a single .mp4.
+`stream_description=''` serves as a text field to optionally put a description for the stream.  No character limit.
 
-+ **Customizable resolution:** You have complete control of the size of the outputted frames, whether they are 480p or
-8K.
-+ **Customizable framerate:** Currently supports 30 and 60 FPS, custom values are coming soon.
+`output_directory=None` is a string for the absolute path of an existing directory you'd like the stream to output to.is where you can optionally define the path of where the created media is outputted.  By default, the stream outputs in a "Render Output"
+ folder within the library's directory.
 
-![Custom Color Showcase](https://i.imgur.com/4uQTxwT.png)
+`output_mode='video'` controls the type of output you will have created.  Your two choices are `'video'`,
+or `'image'`.  Video outputs a single .mp4 file, whereas image output returns all of the frames.
 
-+ **Custom Color Palettes:** The included default palettes are just a starting point.  Make any color palette that you
-want to match the aesthetic where it's being used.  Anyone reading the stream will have the palette automatically saved
-to their machine, so then they can use it as well!  Functionality is included to output a text file outlining all of the
-color palettes you have available to use, both default and custom.
+`stream_name_file_output=False` controls if outputted files will use the stream's SHA-256 hash as a name, or the 
+provided name (`stream_name`) for the stream.  By default, it uses the SHA-256, a 64 character hexadecimal 'fingerprint'
+of the stream.
 
-### Reading
+`max_cpu_cores=0` determines how many CPU cores you'd like to use when rendering frames.  0 is default, which is
+maximum.
 
-+ **Error correction against compression or corruption:** BitGlitter protects your file against corruption and artifacts 
-on the image or video. After loading the correct palette, whenever it detects an incorrect color, it will "snap" it to 
-the nearest color in the palette.  This gives your file resistance against format changes, codecs, or file size 
-reduction.  This allows BitGlitter streams to still be read in environments that would otherwise render all existing 
-steganography methods unreadable.
+`compression_enabled=True` enables or disables compression of your data, prior to rendering into frames.  
+This is enabled by default.
 
-+ **Complete file integrity:** When the stream is created, a hash (SHA-256) is taken of the entire stream, as
-well as each frame.  The data must match what is expected to be accepted.  Damaged or corrupt files will not be blindly
-passed on to you.
+`encryption_key=''` optionally encrypts your data with AES-256.  By default, this is disabled.  The stream will not be
+able to be read unless the reader successfully inputs this.
 
-+ **Streamlined frame bypassing:**  If a frame cannot or doesn't need to be read (ie, a duplicate already read), the 
-reader determines this from an initial frame header
+`file_mask_enabled=False` toggles whether you want the stream manifest to be encrypted or not when your stream itself is
+encrypted.  What this means is simpler terms is the contents of the stream will be hidden until or unless the correct
+encryption key is inputted.  If set to False, the full contents of the stream will be visible.  This only does anything
+when an encryption key is used.
 
-### Design
+`scrypt_N=14`, `scrypt_R=8` and `scrypt_P=1` allow you to customize the parameters of the `scrypt` key 
+derivation function.  You shouldn't touch this if you don't know what is.  Only change these settings if you're 
+comfortable with cryptography, and you know what you're doing!  It's worth noting `scrypt_N` uses its argument as 2^n.
+Finally, if you're changing these numbers, they MUST be manually added when using read functionality, otherwise 
+decryption will fail.  Custom values are deliberately not transmitted in the stream for security reasons. Your end users
+of the stream must know these parameters if they are changed, otherwise BitGlitter will use the default parameters when
+decrypting.
 
-+ **No metadata saved in the file:**  Compress the stream, change formats, upload it somewhere.  All data is encoded in
-the blocks, so you don't have to worry (as much) about rendering the stream unreadable.
+`stream_palette_id='6'` sets the palette used in the stream, after initialization headers are ran.  Takes a string of
+the palette's palette_id.
 
-+ **Easy to understand:** Whether you're learning about Python and want to understand how it works, or you're looking to
-contribute, docstrings and notes are throughout the library.
+`pixel_width=24` sets how many pixels wide each 'block' is when rendered on screen.  24 pixels is default.  This is one 
+of those values that have a large impact on readability.  Having them overly large will make reading it easier, but will
+result in less efficient frames and require substantially longer streams.  Making them very small will greatly increase 
+their efficiency, but at the same time a lot more susceptible to read failures if the files are shrunk, or otherwise
+distorted.  This default value offers a nice middleground.
 
-+ **Built in future-proofing:** As of now, BitGlitter has a single protocol (Protocol 1), which is a specific set of
-  procedures around how data is handled, and the components of a frame, as well as their layout.  Each protocol has its
-  own unique ID to identify it with.  This ID is added in the  header during the write process, and is picked up at 
-  `read()`.  As new protocols get created, older versions of BitGlitter that don't have these included will notify the user
-  to update their version in order for it to be read.  All older protocol versions are saved in future library
-  iterations, so no matter how old the protocol version is used on the stream, it will always be able to be read.
+`block_height=45` sets how many blocks tall the frame will be, by default this is set to 45 (which along with 
+`block_width`, creates a perfect 1080p sized frame).
 
-+ **Fully modular design:** Do you have a specialized use case?  Adapting this library to your own needs is quite easy.
-  I've built BitGlitter to be easy to modify and expand upon.  Rather than worrying about the lower-level functionality,
-  achieve your goal with the modular components I've created.
+`block_width=80` sets how many blocks wide the frame will be.  By default this is set to 80.
 
-### CLI
+`frames_per_second=30` sets how many frames per second the video will play at, assuming `output_mode = "video"` is used.
 
-Write from commnad line:
+Finally we have several arguments to control logging.
 
-+ `python3 -m bitglitter write`
+`logging_level='info'` determines what level logging messages get outputted.  It accepts three arguments- `info` is
+default and only shows core status data during `read()` and `write()`.  `'debug'`  shows info level messages as well as
+debug messages from the various processes.  Boolean `False` disables logging altogether.
 
-      `-file` - The absolute path to the file
-      `-mode` - Either 'image' or 'video'. Default: video
-      `-o` - Output path
+`logging_stdout_output=True` sets whether logging messages are displayed on the screen or not.  Only accepts booleans.
+Enabled by default.
 
- Read from commnad line:
+`logging_txt_output=False` determines whether logging messages are saved as text files or not.  Only accepts type 
+`bool`. Disabled by default.  If set to `True`, a log folder will be created, and text files will be automatically saved
+there.
 
-+ `python3 -m bitglitter read`
-    
-      `-file` - The absolute path to the file
-      `-o` - Output path
+`save_statistics=False` saves some fun statistics about your usage of the program- total number of blocks rendered,
+total frames rendered, and total payload data rendered.  This updates after each successful write session.  Functions
+to interact with this data are below.
 
-### Applications
-To be determined.  This will be updated as time progresses!
+**These default values transmit data at about 80 KB/s.**  This is a safe starting point that should be pretty resistant to
+corruption.
 
-# How to use
+### read() -- converting BitGlitter streams back into directories and files
 
-All of the functions to run this library are explained below.  I'm also working on several Wikipedia pages, explaining
-BitGlitter in greater detail (how it works, etc) with some included illustrations.  These are not yet complete, 
-[but here is the link to the project's Wiki if you'd like to see it!](https://github.com/MarkMichon1/BitGlitter/wiki/Using-BitGlitter)
+`read()` is how we input BitGlitter streams (whether images or video), and output the files and directories encoded in
+them.  There are several other functions included to interact with these streams (changing the encryption key to decrypt
+the stream, removing one or all streams, changing its save path, etc).  Check out **Read Functions** below to learn more.
 
-### Installation
+Like with `write()`, the only argument required is the input path (`file_path`), except in this case it only accepts 
+files.  Supported video formats are `.avi, .flv, .mov, .mp4, .wmv` and supported image formats are `.bmp, .jpeg, .jpg, 
+.png, .webp`.  Can accept a string with a single absolute file path (image or video), or a list of strings of absolute 
+file paths.  Lists can only contain image files, videos must be one at a time.  **Important:**  When inputting image
+files, it is important to add the first few frames containing metadata FIRST, before adding the rest of the standard
+payload type frames.  This metadata gives the reader important data on palettes, stream configuration, and on the payload
+itself.  Some frames may be recognized as corrupted when this data is lacking.  Once metadata is received, the order of
+images to input becomes irrelevant, and the library takes care of the rest.
 
-In addition to downloading the code from Github, you can also grab it directly from PyPI:
+`stop_at_metadata_load=False` This will break out of the function *if* metadata for the stream is read.  This allows you
+to view the metadata and manifest (file/directory contents) of the stream itself, to verify the values for yourself.  From
+there, you can choose to either re-read the file and continue getting the files, or delete it.  This is a security feature
+to double check what the contents are, before you extract the files onto your computer.  Please note that this will only
+run **once** per stream.  When re-ran, the stream will continue to read and decode the files (if enabled in the arguments).
 
-`pip install bitglitter`
+`auto_unpackage_stream=True` controls whether files embedded in the stream should be extracted during read as the frame data
+becomes available to do so.  This lets you extract all files that are available each time read concludes if enabled.  If
+disabled, you can unpackage files using the function `unpackage(stream_sha256)`.  For more information, go to **Read Functions**
 
-**IMPORTANT:** The only part you will need to grab manually is a copy of ffmpeg.exe .  Place it in the same folder the 
-code will be running, and you'll be set.  This will be done automatically in the near future.  Get the package here on
-the left side of the screen:
+`auto_delete_finished_stream=True` deletes all stream metadata and temporary files from your system once the read is
+complete, leaving you only with the decoded files pulled from the stream.  In most cases you probably wouldn't change this,
+but in the event you want to view the data, you can do so here.
 
-[https://ffmpeg.org/download.html](https://ffmpeg.org/download.html)
+`output_directory=None` sets where files will be written as they become available through decoding.  Takes in a string
+of an absolute path of an existing file directory.  This is "set and forget", in the sense the first time a stream is
+recognized/created during read, the output path will be bound to the stream.  Subsequent read() calls for other frames 
+will continue to have the outputted files going to the right place.
 
-ffmpeg.exe is all that is needed.
+`bad_frame_strikes=25` Sets how many corrupted frames the reader is to detect before it aborts out of a video.  
+This allows you to break out of a stream relatively quickly if the video or images are substantially corrupted, without 
+needing to iterate over each frame.  If this is set to 0, it will disable strikes altogether and attempt to read every 
+frame regardless of the level of corruption.
 
-**Required Third Party Libraries**
+`max_cpu_cores=0` determines the amount of CPU cores to use, like `write()`.  The default value of 0
+sets it to maximum available.
 
-+ `bitstring` - Bit manipulation.
-+ `cryptography` - Cryptographic functions.
-+ `ffmpeg-python` - Video rendering and output.
-+ `opencv-python` - Video loading and frame manipulation.
-+ `Pillow` - Frame creation and output, as well as loading images and reading pixel values.
+`block_height_override=False` and `block_width_override=False` allow you to manually input the stream's block height and 
+block width.  Normally you'll never need to use this, as these values are automatically obtained as the frame is locked
+onto.  But for a badly corrupted or compressed frame, this may not be the case.  By using the override, the reader will
+attempt to lock onto the screen given these parameters.  Both must be filled in order for the override to work.
 
-Thanks to Tanmay Mishra for giving me a pre-release version of his upcoming library `filepackager`.  It has been heavily
-modified and stripped down to suit this library.  The code is included with BitGlitter; there is no need to download it.
+`encryption_key=None` is where you add the encryption key to decrypt the stream.  Like argument `output_directory`, you only
+need this argument once, and it will bind to that save.
 
-**BitGlitter in 60 seconds**
+`scrypt_n=14`, `scrypt_r=8`, and `scrypt_p=1` are values that control scrypt password hashing if your stream is encrypted.
+These only need to be touched if the stream creator changed the default values during `write()`.  IF that is the case,
+these values must be identical to the values used during write; decryption won't work even with the correct encryption
+key.  Please note that you can change these values even after `read()` `with attempt_password()`.  See **Read Functions**
+below for more information.
 
-Even though it comes shipped with a lot of functionality, all you need to use it is `write()` (creates the streams) and 
-`read()` (which reads them and extracts the data encoded in it).  The only required argument for both is the file you
-wish to input in string format.
+`logging_level='info'`, `logging_stdout_output=True`, `logging_txt_output=False`, and `save_statistics=False` are 
+arguments as well.  These are seen in `write()` as well; read their descriptions above to see what these do.
 
-### write(), converting files into BitGlitter streams
+### Custom Color Palette Functions
 
-We'll go a bit more in depth now.
+If you aren't happy with the 8 'official' palettes included with the library, you also have the freedom to create and 
+use your own, to have them match with whatever aesthetic/style or performance you want.  The entire process is very simple.
+There is nothing you have to do for other people to read streams using your custom palettes; the software automagically
+'learns' and adds them through a special header on the stream, which then gives the ability for others to use your palette
+as well.  If you want to share your palette with others without them needing to read a BitGlitter stream, we got you 
+covered.  Custom palettes can also be imported and exported with [Base64](https://en.wikipedia.org/wiki/Base64) encoded 
+share strings.
 
-`write()` is the function that inputs files and turns them into a BitGlitter stream.  There are quite a few arguments
-to customize the stream, but there is only one required argument.  Everything else has defaults.
+`add_custom_palette(palette_name, color_set, nickname='', palette_description='')` Creates a custom palette.  Once it has
+been created, a string of its unique ID is returned to you (a SHA-256 of its values as well as a timestamp, making it more
+or less entirely unique to that palette).
 
-Not surprisingly, that required argument defines what files or folders you wish to embed in the stream.  If and only if
-you're sending a single file or folder path, argument `fileList` takes a string of the path.  BitGlitter also supports
-sending multiple files and folder together, of which there is no limit!  This would require using a tuple or list item
-filled with strings of the file or folder paths.  File or folder paths that don't exist are automatically ignored.
++ `palette_name` is the its name which will it will be saved as.  It has a max length of 50 characters.
 
-`stream_name=''` is what you can use to optionally title your stream, which will be printed out on the screen of whoever
-reads the file, along with other stream data.
++ `palette_description` is an optional field to include a brief description that will be attached with it.  It has a max
+length of 100 characters.
 
-`stream_description=''` serves as a text field to optionally put a description for the stream.
++ `color_set` Is the actual colors that will be used in it.  It can be a list of lists, or a tuple of tuples (no 
+difference) of [RGB24](https://en.wikipedia.org/wiki/RGB_color_model) values.  Heres an example to give you a better 
+idea: `color_set=((0, 255, 0), (0, 0, 255))`.  There are a few constraints you must follow:
+  + No two identical values can be added.  For instance, the color black with the same RGB values twice.  Each color used
+must be unique!  The more 'different' the colors are in terms of their values, the better.
+  + A minimum of two colors must be used.
+  + You must have 2^n total colors (2, 4, 8, 16, 32, etc), with up to 256 currently supported.
 
-`output_path=False` is where you can optionally define the path of where the created media is outputted.  By
-default, media is saved where the python file is ran.  The folder path must already exist if used.
++ `nickname` is an optional field that is a shorter, simple way to remember and use the palette, rather than its long
+generated ID.  Both serve as a unique way to identify the palette.
 
-`output_mode='video'` is where you define how you wish the stream to output, whether as an .mp4 video, or a series of
-.png images.  The only two valid arguments are `'image'` and `'video'`.
+`return_palette(palette_id=None, palette_nickname=None)` Returns a dictionary object of all of the palettes values.
 
-`compression_enabled=True` enables or disables compression of your data, prior to rendering into frames.  This is enabled
-by default.
+`edit_nickname_to_custom_palette(palette_id, existing_nickname, new_nickname)` Allows you to change the nickname of the 
+palette.  Please note that the nickname must be unique, and no other palettes may already be using it.
 
-`file_mask_enabled=False` is where you can omit the listing of files/folders from the stream header.  This effectively
-hides the contents of the stream, unless it is fully read.  By default, this is disabled.  What this means is when
-someone reads your stream, in the first several frames it will automatically display the contents of the stream (files
-as well as their size) on the screen.
+`export_palette_base64(palette_id=None, palette_nickname=None)` Export any of your custom palettes using this.  It returns
+a share code which anyone can use to import your palette.
 
-`encryption_key=''` optionally encrypts your data with AES-256.  By default, this is disabled.  The stream will not be
-able to be read unless the reader successfully inputs this.
+`import_palette_base64(base64_string)` Import palettes from a unique share code (see directly above).
 
-Arguments `scrypt_N=14`, `scrypt_R=8` and `scrypt_P=1` allow you to customize the parameters of the `scrypt` key derivation 
-function.  If you're a casual user, you'll never need to touch these (and shouldn't).  Only change these settings if
- you're comfortable with cryptography and you know what you're doing!  It's worth noting `scrypt_N` uses it's argument as
- 2^n.  Finally, if you're changing these numbers, they MUST be manually inputted during `read()` otherwise decryption
- will fail!  Custom values are deliberately not transmitted in the stream for security reasons.  Your end users of the
- stream must know these custom parameters.
+`generate_sample_frame(directory_path, palette_id=None, palette_nickname=None, all_palettes=False, include_default=False,
+pixel_width=20, block_height=20, block_width=20)` 
+Generates a small 'thumbprint' frame of a palette, giving you an idea of how it would appear in a normal rendering.  
+`directory_path` is an existing directory in which it will be saved. `all_palettes` toggles whether you want to get a 
+sample from a specific palette (using `palette_id` or `palette_nickname`) or all palettes saved.  `include_default` toggles
+whether you want to include all default palettes in the generated output, or if you only want to generate custom palettes.
+The last 3 arguments let you control the exact size of the frames.  You can also use this function to generate artwork
+or cool looking wallpapers using the palettes as well.
 
-`header_palette_id='6'` sets the palette used in the 'setup' frames in the beginning.  It is strongly recommended you use
-a default palette here if you don't know what you're doing, because this is where important information regarding the
-stream is read, and by using a custom palette, it will be impossible for anyone to read it who hasn't already 'learned'
-the palette.
+`return_all_palettes()` Returns a list of dictionary objects of all palettes in your database.
 
-`stream_palette_id='6'` sets the palette used for the payload.  By default, the 4 bit default color set is used.  I'll 
-explain all about palettes below.
+`return_default_palettes()` Returns a list of dictionary objects of all default palettes in your database.
 
-`pixel_width=24` sets how many pixels wide each block is when rendered.  By default it's 20 pixels.  This is a very
-important value regarding readability.  Having them overly large will make reading them easier, but will result in less
-efficient frames and require substantially longer streams.  Making them very small will greatly increase their
-efficiency, but at the same time a lot more susceptible to read failures if the files are shrunk, or otherwise
-distorted.
+`return_custom_palettes()` Returns a list of dictionary objects of all custom palettes in your database.
 
-`block_height=45` sets how many blocks tall the frame will be, by default this is set to 45 (which along with 
-`block_width`, creates a perfect 1080p sized frame).
+`remove_custom_palette(palette_id, nickname)` Deletes a custom palette.
 
-`block_width=80` sets how many blocks wide the frame will be.  By default this is set to 96.
+`remove_custom_palette_nickname(palette_id, existing_nickname)` Removes the nickname from a given palette.  This doesn't
+remove the palettes themselves, and they can still be accessible through their palette ID.
 
-`frames_per_second=30` sets how many frames per second the video will play at, assuming argument `output_mode = "video"`.
-Currently, 30fps and 60fps are accepted.
+`remove_all_custom_palette_nicknames()` Removes all nicknames from all custom palettes.  As said directly above, this only
+removes the nickname, not the actual palette.
 
-Finally we have several arguments to control logging.
+`remove_all_custom_palettes()` Deletes all custom palettes, leaving only the default (hardwired) palettes.
 
-`logging_level='info'` determines what level logging messages get outputted.  It accepts three arguments- `info` is
-default and only shows core status data during `read()` and `write()`.  `'debug'`  shows INFO level messages as well as
-lower level messages from the various processes.  Boolean `False` disables logging altogether.
+### Read Functions
 
-`logging_screen_output=True` sets whether logging messages are displayed on the screen or not.  Only accepts type `bool`. 
-Enabled by default.
+During the read process, persistent data is stored in a sqlite database tracking its state.  These functions give you a
+look inside, as well as some greater control of the reads themselves.  BitGlitter automatically deletes temporary byte
+data for frames as soon as it can (ie, files can begin to be unpackaged).  What remains of finished streams is a small,
+minimal view of their internal state, as well as stream metadata.  Be aware that the `read()` argument 
+`auto_delete_finished_stream=True` (default) will automatically delete these when the stream is fully decoded (ie, all frames
+are accounted for).  For more information read about `auto_delete_finished_stream` above.
 
-`logging_save_output=False` determines whether logging messages are saved as text files or not.  Only accepts type `bool`.
-Disabled by default.  If set to `True`, a log folder will be created, and text files will be automatically saved there.
+Final note before proceeding- many of these functions you'll see `stream_sha256`; this is a string of the stream's 
+SHA-256 hash.
 
-These default values have an 81KB/s transmission rate.  This is only a starting point that should be pretty resistant to
-corruption.
+`unpackage(stream_sha256)` If `unpackage_files=False` was an argument in read(), this will unpackage the stream (or as
+much as it can from what has been scanned and decoded).  A dictionary object will be returned that either outlines
+the actions taken, or the error(s) why unpackaging cannot take place (yet)
 
-### read(), converting streams back into data
+`return_single_read(stream_sha256, advanced=False)` Returns a basic dictionary object of stream read's state. Some fields
+may be empty depending on its state, or how many metadata headers have been decoded so far.  Setting `advanced` to 
+`True` will return all state data (for development, debugging, or if you're just curious).  Returns `False` if SHA-256 
+matches no existing stream.
 
-`read()` is what you use to input BitGlitter streams (whether images or video), and will output the files.
+`return_all_read_information(advanced=False)` Returns a list of dictionary objects of all of the stream read states
+in your database.  For more information on what `advanced` does, look directly above this.
 
-Like with `write()`, the only argument required is the BitGlitter-encoded file, whether that's an image or a video.
-`file_to_input` is the only required argument.  We'll go over the other ones.
+`update_decrypt_values(stream_sha256, decryption_key=None, scrypt_n=None, scrypt_r=None, scrypt_p=None)`
+Updates values to decrypt the stream.  From here, you have two paths:  If file masking is enabled on the stream,
+`attempt_metadata_decrypt()` will decrypt the metadata header (necessary for extracting files from binary data), and
+from there you can `unpackage()`.  Otherwise if file masking is disabled but the stream is encrypted, you can go straight
+to unpacking.
 
-`output_path=None` Is where you can set where this stream will be saved once all frames have been successfully loaded.
-  It's 'set and forget', so if you are loading images this argument only has to be used once, and the folder path will
-  stick with that stream.  This argument requires a strong of a folder path that already exists.
+`attempt_metadata_decrypt(stream_sha256)` will attempt to decrypt the metadata from your read stream (if it is
+encrypted with file masking enabled), using your supplied decryption parameters (decryption key, scrypt N, scrypt R,
+scrypt P).  Returns a dictionary object with the decoded metadata; if there is any error, it will return a brief explanation.
 
-`bad_frame_strikes=10` This sets how many corrupted frames the reader is to detect before it aborts out of a video.  This
-allows you to break out of a stream relatively quickly if the video is substantially corrupted, without needing to
- iterate over each frame.  If this is set to 0, it will disable strikes altogether, and attempt to read each frame 
- regardless of the level of corruption.
+`return_stream_manifest(stream_sha256, return_as_json=False)` Returns an overview of all files included in the given 
+stream, as well as their file size, and SHA-256 hash.  Nested directory structures (if applicable) and file data are 
+included in the returned object.  Keys are quite short to minimize manifest size when being transmitted.
 
-`block_height_override=False` and `block_width_override=False` allow you to manually input the stream's block height and 
-block width.  Normally you'll never need to use this, as these values are automatically obtained as the frame is locked
-onto.  But for a badly corrupted or compressed frame, this may not be the case.  By using the override, the reader will
-attempt to lock onto the screen given these parameters.  Both must be filled in order for the override to work.
++ For directories:  `n` directory name, `f` files in that directory (not including subdirectories), `s` subdirectories for
+that given directory.
 
-`encryption_key=None` is where you add the encryption key to decrypt the stream.  Like argument `output_path`, you only
-need this argument once, and it will bind to that save.
++ For files: `fn` file name, `rs` raw file size (its true size), `rh` raw file hash (its true SHA-256 hash), `ps` 
+processed file size (its packaged size when being transmitted), `ph` processed file hash (its packaged SHA-256 hash when 
+being transmitted). Files are compressed in transit (unless you explicitly disable it in `write()` settings), hence the
+alternate size and hash for them.
 
-Arguments `scrypt_n=14`, `scrypt_r=8` and `scrypt_p=1`
+`return_as_json` controls the format of the returned output.  If set to `False`, it returns a dictionary object; if 
+`True`, it will send a JSON string.
 
-`logging_level = 'info'`, `logging_screen_output = True`, `logging_save_output = False` - Please see the full explanation at
-`write().`
+`remove_partial_save(stream_sha256)` Completely removes the stream read from the database.  Be aware that read argument
+`auto_delete_finished_stream` automatically does this if enabled for the stream.
 
-### Color Palettes
+`remove_all_partial_save_data()` Removes all stream reads from the database.
 
-If you wish to make your own custom color palettes, BitGlitter gives you the ability to do that with these functions.
+`update_stream_read(stream_sha256, auto_delete_finished_stream=None, auto_unpackage_stream=None)` Is where you can
+update behavior when the stream unpackages.
 
-`add_custom_palette(palette_name, palette_description, color_set, optional_nickname = "")`  This function adds custom palettes
-to use.  
+`blacklist_stream_sha256(stream_sha256)` Disallow a specific SHA-256 hash of a stream to be read on your client.  Will
+also remove the Stream Read containing that hash as well, if it exists.
 
-Argument `palette_name` takes a string and is the name of the palette that gets displayed and transmitted. 
+`return_all_blacklist_sha256()` Returns a list of all blacklisted SHA-256 hashes as strings.
 
-Argument`palette_description` takes a string as well, and is the description of the palette, if you wish to add it.  
+`remove_blacklist_sha256(stream_sha256)` Removes a specific SHA-256 hash.  Returns `True` or `False` depending on whether
+it existed.
 
-Argument `color_set` takes a tuple of RGB tuples, these will be the actual colors used in the BitGlitter stream.  Here's 
-a simple example of what it would look like using two colors: `color_set=((0, 255, 0), (0, 0, 255))`.  There are a few
-requirements to these tuples:
-+ No two identical values can be added.  For instance, the color black with the same RGB values twice.  Each color used
-must be unique!  The more 'different' the colors are, the better.
-+ You must have a minimum of two colors.
-+ It must be 2^n colors used, so 2, 4, 8, 16, etc.
+`remove_all_blacklist_sha256()` Removes all SHA-256 hashes from the blacklist.
 
-Argument `optional_nickname=""` allows you to use an easy to input nickname for your custom palette.  This nickname is 
-  how you select this palette to specifically run on your stream.  Internally, custom palettes have a 64 character ID 
-  code which you can use (more on this below).  This allows you to give it a string of your choosing to designate it as 
-  well.  This field is optional.  If you do decide to use it though, both the internal ID AND the nickname will work.
+`return_stream_frame_data(stream_sha256)` Returns a list of dict objects with data about all read/decoded frames.  Warning:
+depending on stream size, this may be a long/resource intensive function to perform.
 
-`edit_nickname_to_custom_palette(id_or_nick, new_name)` This function allows you to edit the nickname of your custom palette 
-to something else.  Both arguments require strings.  You can use it's nickname you assigned it, or it's internal ID.
+`return_stream_file_data(stream_sha256)` Returns a list of dict objects with data about all files read from the manifest.
+You'll get access to their name, file size, SHA-256 hash, whether they are eligible to be unpackaged, if they were already
+unpackaged, and other less user-friendly internal state data.
 
-`print_full_palette_list(path)` This function outputs a text file to the folder path outlining the palettes available, both
-default palettes and custom.  It shows information such as their name, description, date created, color set, nickname,
-and more.  The required argument is a string of a folder path, which must already exist.  Here's an example of how to
-format it: `C:\Users\Mark\Desktop`
+`return_stream_progress_data(stream_sha256)` Returns a list of dict objects with data about current 'slices' of the stream
+you're able to unpackage.  These index numbers are 0-based and are for bits; this is the internal system BitGlitter uses
+to determine eligible files.
 
-`clear_all_custom_palettes()` This removes all custom palettes from your config.  Please note that the default palettes 
-will not be removed.
+`verify_is_bitglitter_file(file_path)` Allows you to quickly test whether a given image or video is a valid BitGlitter 
+stream or not, without relying on `read()` and possibly saving anything to the database.  Attempts to lock onto the 
+frame and read/decode the initializer header; it will return a boolean value whether that was successful or not.
 
-`remove_custom_palette(id_or_nick)` This function removes the custom palette from your config.  It takes a string argument
-of either it's internal ID, or a nickname you've previously given it.
+### Preset Functions
 
-`remove_custom_palette_nickname(id_or_nick)` This function strips any nickname associated with a custom palette.  It takes a
-string argument of either the internal ID or a previous nickname.
+Presets allow you to define `write()` behavior (geometry, palettes, etc) and save it with a nickname, so you can 
+quickly and easily use your favorite configurations with a single string, without needing to explictly state all 
+parameters every time.
 
-`clear_custom_palette_nicknames()`  This removes all nicknames from all custom palettes.
+`add_new_preset(nickname, output_mode='video', compression_enabled=True, scrypt_n=14, scrypt_r=8, scrypt_p=1,
+stream_palette_id='6', header_palette_id='6', pixel_width=24, block_height=45, block_width=80, frames_per_second=30)`
+is how you add a new preset.  Please note all of its default arguments are identical to default `write` arguments.  For
+more information on each of these arguments, check out the `write()` section for arguments above.
 
-### Partial Save Control
+`return_preset_data(nickname)` returns a dictionary object returning the full state of the preset.
 
-Once the first frame of a stream is read, a PartialSave object is created.  This is essentially what manages the binary
-strings, and holds various information on it's state.  These functions help better interface with them.
+`return_all_preset_data()` returns a list of all of the presets as dictionaries.
 
-`update_partial_save(stream_sha, reattempt_assembly = True, password_update = None, scrypt_n =  None, scrypt_r = None, 
-scrypt_p = None, change_output_path = None)` This function allows you to update various parameters of the save.  Requires
-a string input for `streamSHA` which is the ID number of the string.  
+`remove_preset(nickname)` removes the preset with the `nickname` you gave it.
 
-Argument `reattempt_assembly` makes the assembler
-attempt to reassemble the frames, as well as output the embedded files and folders.  This would be used in the case of
-an incorrect password or scrypt parameters, and you'd like to try again.
+`remove_all_presets()` removes all saved presets.
 
-Argument `password_update` takes a string argument, and will add (or replace) the encryption key tied to this stream.
+### Statistics Functions
 
-Arguments `scrypt_n, scrypt_r and scrypt_p` change the scrypt parameters used to derive the key used for decryption.  If
-the scrypt parameters were left at default during `write()` of the stream, these can be left as is.  Otherwise, the 
-custom values will need to be inputted whether here or in the optional arguments of `read()`.
+`output_stats()` Returns a dictionary object displaying the following data for both reads and writes: blocks processed,
+frames processed, data processed.
 
-`begin_assembly(stream_sha)` This function exists to initiate assembly of a package at a later time, rather than doing so 
-immediately for whatever reason.
+`clear_stats()` All statistics reset back to zero.
 
-`print_full_save_list(path, debug_data=False)` This function outputs a text file to the folder path outlining all (if any)
-partial saves you have on your system.  You can check their status, as well as the state of the PartialSave object 
-itself.  Argument `debugData` is `False` by default, but enabling it to `True` outputs various debug information 
-pertaining to the object as well, which wouldn't serve much utility seeing for someone such as a casual end user.
+### General Functions
 
-`remove_partial_save(stream_sha)`  Using a string argument for the stream's ID (or stream SHA, as commonly used), this will
-remove the object from your config, as well as remove all temporary data associated with it.
+`remove_session()` Resets the entire state of the library.  Removes statistics data, all saved streams, saved presets,
+and saved palettes.  All settings get reverted to default, and default/included palettes are re-added.  This can't be undone.
 
-`remove_all_partial_saves()` All saves are removed from the config object, clearing all temporary data.
+`return_settings()` Returns a dictionary object of all settings.
 
-### General Configuration
+`update_settings(read_path=None, read_bad_frame_strikes=None, enable_bad_frame_strikes=None, 
+                    write_path=None, log_txt_path=None, log_output=None, logging_level=None, maximum_cpu_cores=None,
+                    save_statistics=None, output_stream_title=None)` Allows you to update any of the settings.  Use caution
+when changing these, as it could potentially result in crashes for invalid values.
 
-`outputStats(path)` This function gives you a neat bird's eye view of your BitGlitter usage.  During all `read()` and 
-`write()` cycles, the total amount of data transferred, as well as total frames transferred and individual blocks 
-scanned gets added to a running total.  Argument `path` requires a string argument of a folder path that already exists.
-A small text file will be written to this location.
+![Splitter](https://i.imgur.com/tozbtUz.png)
 
-`clear_stats()` All statistics will be reset to zero.
+### Roadmap
 
-`clear_session()` This wipes all inputted data.  Custom colors, statistics, and partial save objects will all be erased,
-as well as any temporary data for the partially read streams.  This is in essence a hard reset.
+Here are a few possible directions this can move in which would increase its usefulness and versatility:
+- **Command line functionality:** Ability to read and write BitGlitter streams as well as use the rest of the built-in
+functions, straight from the terminal.
+- **"Splash Screen":** At the end of streams, include some cool looking rendered animation with the project logo, a 
+brief explanation of what it is, and a URL to download the software.  People not knowing what BitGlitter is will now
+have an idea as well as a way to download it, increasing usage and fueling development of the project.  Could also
+include metadata about the stream itself.
+- **Inline streams:** Have a stream embedded in another (non-BitGlitter) video, allowing for data to be read inside
+a normal, human-friendly video.  Rather than taking up the full video screen, the stream could be a bar on the top or
+bottom of the screen, or any arbitrary shape (perhaps even animated).  This allows content creators to 'attach' files
+to their videos, much like you can attach arbitrary files to an email.
+- **Moving heavy lifting away from Python:** While libraries like `cv2` and `numpy` are used which utilize C++, pure
+Python is used in a few heavily used functions (thousands of times per second).  Moving these to Rust or C++ would 
+substantially speed up the software, and make new use cases possible...
+- **Livestream capabilities:** BitGlitter streams can be 'broadcast' over live video.  Reader would be able to detect
+data streams through visual or audio cues from the multimedia, and can process/decode on the fly.  BitGlitter streams 
+are no longer restricted to 'static' files, but are open to any kind of live-streaming data, which can be optionally
+compressed/encrypted through the original feature set.
 
-# Contributing
+### Contributing
 
-Whether you're a seasoned programmer or brand new, there's plenty of things you can do to help this project succeed.
-Join our discord server, and check out all of the information I posted in the "Information" category.  Thank you for
-your interest!
+**Let me know of your ideas and suggestions!**  There are many directions this technology can go in, and with enough interest
+your ideas can be future additions to this core library (as well as the desktop app).  If you're looking to help with 
+developing it.... awesome.  All I ask is you're skilled with Python, and can write clean and structured code.  I went
+out of my way to have clear variable and function names as well as a decent amount of comments scattered throughout the
+library- it should be relatively easy to get up to speed to understand how BitGlitter works underneath the hood.
 
-**Discord Link**
+Drop in and say hi on the Discord server:
 
 **https://discord.gg/t9uv2pZ**
 
-Also, be sure to check out the 
-[contributing master page](https://github.com/MarkMichon1/BitGlitter/wiki/Contributing-Master-Page).  It contains a lot
-of information.
+### Disclaimer
+
+There are a few points worth bringing up on using this library:
 
-![Splitter](https://i.imgur.com/qIygifj.png)
++ **Output sizes can be huge.** At the end of the day, we're using far less dense storage mediums (color data in image and video 
+frames) to hold and transmit good amounts of data.  The payback, however, is giving you much greater portability in how
+or where you can store, host, and transmit it.  Depending on the settings, stream size can be 1.01x-100x+ the size of the
+payload itself.  Start with relatively smaller files when starting out with this library to get a better feel for this.  
+For now until we get a better feel for how people are using it and how things can be fine-tuned, its a fair assumption that
+BitGlitter is best suited for payloads less than 50-100MB.
++ **This library is completely experimental:** In its current state, BitGlitter is merely a proof of concept to see if
+doing this was possible and if there is interest.  It started as one of my first projects learning Python and programming
+as a whole.  There is made by one person, but I'm doing my best to create a well-rounded product that accomplishes what 
+its designed to do.
 
-### Practical Limits
+Please use the issues page or let me know on Discord if there is something not working.
 
-It's worth stating the constraints you may face while using this.  While the images and video BitGlitter exports are
-lossless (no compression applied), the "real world" on the internet is much different.  For instance, multimedia 
-uploaded to popular social media sites is regularly compressed in order to save space (and ultimately cut down on 
-expenses).  You are protected *to an extent* with BitGlitter from this.  Write parameters are fully customizable
-primarily for this reason.  
+### Acknowledgements
+Thank you to [Tanmay Mishra](https://github.com/tmishra3) for giving me guidance during planning of the library, as well
+as its initial development. A big thank you to [Jack Robison](https://github.com/jackrobison) as well for his continued
+wisdom.
 
-While you can have greater throughput with larger colorsets, smaller blocks, and faster framerates, there may be a
-practical limit to whether it will work depending on the degree their compression reduces quality.  At the expense of
-throughput, larger blocks, slower framerates, and fewer colors used will make the stream *far* more resistant to
-possible corruption.  Approaching the extreme limits of these parameters (tiny block sizes, very fast framerates, very
-large colorsets), in terms of reading it and converting it back into data, requires very precise measurements of
-position and color value; *a codec's purpose is to blur those precise values to reduce it's bitrate, and in turn it's
-file size.*  While BitGlitter will detect corruption and perform an "emergency stop," I know you don't want to deal with
-that, and neither do the people you're sharing with.
+**The third party libraries that make BitGlitter possible:**
 
-In closing, know the environment the video will be used in to ensure success in reading it.
++ `bitstring` - Bit manipulation
++ `cryptography` - Cryptographic functions
++ `numpy` - Formatting frames as high performance array during read()
++ `opencv-python` - Video loading, frame rendering, scanning and frame manipulation.
++ `SQLAlchemy` - Managing all persistent data
 
 # MIT License
-© 2019 - ∞ Mark Michon
+© 2021 - ∞ Mark Michon
 
 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 
 documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 
diff --git a/bitglitter/__init__.py b/bitglitter/__init__.py
index bd19321..3f74d9c 100644
--- a/bitglitter/__init__.py
+++ b/bitglitter/__init__.py
@@ -1,6 +1,36 @@
+from bitglitter.config.defaultdbdata import load_default_db_data
+load_default_db_data()
+
+# Core Uses
+from bitglitter.write.write import write
+from bitglitter.read.read import read
+
+# General Config
+from bitglitter.config.configfunctions import clear_stats, output_stats, remove_session, return_settings, \
+    update_settings
+
+# Read Config
+from bitglitter.config.readfunctions import attempt_metadata_decrypt, blacklist_stream_sha256, \
+    remove_all_blacklist_sha256, remove_all_partial_save_data, remove_blacklist_sha256, remove_partial_save, \
+    return_all_blacklist_sha256, return_all_read_information, return_single_read_information, return_stream_file_data, \
+    return_stream_frame_data, return_stream_manifest, return_stream_progress_data, unpackage, update_decrypt_values, \
+    update_stream_read, verify_is_bitglitter_file
+
+# Palette Config
+from bitglitter.config.palettefunctions import add_custom_palette, edit_nickname_to_custom_palette, \
+    export_palette_base64, generate_sample_frame, import_palette_base64, remove_all_custom_palettes, \
+    remove_all_custom_palette_nicknames, remove_custom_palette, remove_custom_palette_nickname, return_all_palettes,\
+    return_custom_palettes, return_default_palettes, return_palette
+
+# Preset Config
+from bitglitter.config.presetfunctions import add_new_preset, remove_all_presets, remove_preset, \
+    return_all_preset_data, return_preset
+
+
+# ===============================================================================
 # The MIT License (MIT)
 #
-# Copyright (c) 2019 - ∞ Mark Michon
+# Copyright (c) 2021 - ∞ Mark Michon
 #
 # Permission is hereby granted, free of charge, to any person obtaining a copy
 # of this software and associated documentation files (the "Software"), to deal
@@ -22,35 +52,16 @@
 #
 # ===============================================================================
 #
-# Official project page:
-# https://github.com/MarkMichon1/BitGlitter
+# Python library project page:
+# https://github.com/MarkMichon1/BitGlitter-Python
 #
-# Guides and roadmap (under construction!):
-# https://github.com/MarkMichon1/BitGlitter/wiki
+# Electron App project page:
+# https://github.com/MarkMichon1/BitGlitter
 #
-# Discord channel:
+# Discord server:
 # https://discord.gg/t9uv2pZ
 #
-# Have fun! :)
+# Enjoy! :)
 #
 #   - Mark
 # ===============================================================================
-
-# CORE USES
-from bitglitter.write.write import write
-from bitglitter.read.read import read
-
-
-# GENERAL CONFIGURATION
-from bitglitter.config.configfunctions import clear_session, clear_stats, output_stats
-
-
-# PARTIAL SAVE CONTROL
-from bitglitter.read.savedfilefunctions import begin_assembly, print_full_save_list, remove_partial_save, \
-    update_partial_save, remove_all_partial_saves
-
-
-# CUSTOM PALETTE
-from bitglitter.palettes.palettefunctions import add_custom_palette, edit_nickname_to_custom_palette, \
-    clear_all_custom_palettes, clear_custom_palette_nicknames, print_full_palette_list, remove_custom_palette, \
-    remove_custom_palette_nickname
\ No newline at end of file
diff --git a/bitglitter/__main__.py b/bitglitter/__main__.py
index e4b37f6..16c31bc 100644
--- a/bitglitter/__main__.py
+++ b/bitglitter/__main__.py
@@ -1,7 +1,11 @@
-import argparse
-from .cli.args import parser
+# This module is currently unused, but will remain for the future if there is a demand for CLI functionality.
 
-
-
-if __name__ == '__main__':
-	parser.parse_known_args()
\ No newline at end of file
+# import argparse
+# from .cli.args import parser
+#
+# def cli():
+#     parser.parse_known_args()
+#
+#
+# if __name__ == '__main__':
+#     cli()
diff --git a/bitglitter/cli/analyzer.py b/bitglitter/cli/analyzer.py
index 51c2f23..75491f2 100644
--- a/bitglitter/cli/analyzer.py
+++ b/bitglitter/cli/analyzer.py
@@ -1,54 +1,53 @@
-from os import path
-from bitglitter.write.write import write
-from bitglitter.read.read import read
+# This module is currently unused, but will remain for the future if there is a demand for CLI functionality.
 
-
-class CliArgumentParser(object):
-    """
-    This class should be responsible
-    to parse all CLI args.
-    """
-    
-    # TODO: To check if output path is a valid path and hold
-    # possible exceptions that may occur there just like write and read path. 
-
-
-    @staticmethod
-    def read(arguments:dict):
-        file = arguments.get('file')
-        output_path = arguments.get('o')
-        
-        if not output_path:
-            output_path = False
-        
-        if path.isfile(file) == True or path.isdir(file) == True:
-            read(fileToInput=file)
-        else:
-            raise FileNotFoundError('Read first argument must be a valid path to a file or folder.')
-
-    
-    
-    @staticmethod
-    def write(arguments:dict):
-        file_path = arguments.get('file')
-        mode = arguments.get('mode')
-        output_path = arguments.get('o')
-        stream_name = arguments.get('streamname', '')
-        
-        if not output_path:
-            output_path = False
-        
-        if not mode:
-            mode = 'image'
-        
-        if path.isfile(file_path) == True or path.isdir(file_path) == True:
-            try:
-                write(fileList=file_path, streamName=stream_name, outputMode=mode, outputPath=output_path)
-            except FileNotFoundError:
-                raise FileNotFoundError("ffmpeg is required and wasn't found do you have ffmpeg installed in your local directory ?")
-            except TypeError:
-                raise TypeError('Please provide -streamname argument.')
-            except PermissionError:
-                raise PermissionError(f"You don't have the proper permissions to write at {output_path}, \ntry to change output path or run the command with sudo under Linux")
-        else:
-            raise FileNotFoundError('Write first argument must be a valid path to a file or folder.')
\ No newline at end of file
+# from os import path
+# from bitglitter.write.write import write
+# from bitglitter.read.read import read
+#
+#
+# class CliArgumentParser(object):
+#     """
+#     This class should be responsible
+#     to parse all CLI args.
+#     """
+#
+#
+#     @staticmethod
+#     def read(arguments:dict):
+#         file = arguments.get('file')
+#         output_path = arguments.get('o')
+#
+#         if not output_path:
+#             output_path = False
+#
+#         if path.isfile(file) == True or path.isdir(file) == True:
+#             read(file_path=file)
+#         else:
+#             raise FileNotFoundError('Read first argument must be a valid path to a file or folder.')
+#
+#
+#
+#     @staticmethod
+#     def write(arguments:dict):
+#         file_path = arguments.get('file')
+#         mode = arguments.get('mode')
+#         output_path = arguments.get('o')
+#         stream_name = arguments.get('streamname', '')
+#
+#         if not output_path:
+#             output_path = False
+#
+#         if not mode:
+#             mode = 'image'
+#
+#         if path.isfile(file_path) == True or path.isdir(file_path) == True:
+#             try:
+#                 write(input_path=file_path, stream_name=stream_name, output_mode=mode, output_directory=output_path)
+#             except FileNotFoundError:
+#                 raise FileNotFoundError("ffmpeg is required and wasn't found do you have ffmpeg installed in your local directory ?")
+#             except TypeError:
+#                 raise TypeError('Please provide -streamname argument.')
+#             except PermissionError:
+#                 raise PermissionError(f"You don't have the proper permissions to write at {output_path}, \ntry to change output path or run the command with sudo under Linux")
+#         else:
+#             raise FileNotFoundError('Write first argument must be a valid path to a file or folder.')
\ No newline at end of file
diff --git a/bitglitter/cli/args.py b/bitglitter/cli/args.py
index 4342661..fa8141b 100644
--- a/bitglitter/cli/args.py
+++ b/bitglitter/cli/args.py
@@ -1,72 +1,74 @@
-import argparse
-
-from bitglitter.cli.analyzer import CliArgumentParser
-
-parser = argparse.ArgumentParser(description=""""Embed data payloads inside of ordinary
-                                             images or video with high-performance 2-D barcodes.""")
-
-
-subparsers = parser.add_subparsers(dest="subparser_name")
-
-
-write_parser = subparsers.add_parser(
-     'write',
-     help="""This is the primary function in creating BitGlitter 
-     streams from files.  Please see Wiki page for more information.""",
-)
-
-write_parser.add_argument(
-     '-file',
-     type=str,
-     help='The file in which data will be written.'
-)
-
-write_parser.add_argument(
-     '-streamname',
-     type=str,
-     help='Stream name.'
-)
-
-write_parser.add_argument(
-     '-mode',
-     type=str,
-     help='File output mode: "image" or "video".'
-)
-write_parser.add_argument(
-     '-o',
-     type=str,
-     help='File output path.'
-)
-
-
-# To implement here
-read_parser = subparsers.add_parser(
-     'read',
-     help="""This is the high level function that decodes BitGlitter encoded images and video back into
-               the files/folders contained within them.  This along with write() are the two primary
-               functions of this library."""
-)
-
-read_parser.add_argument(
-     '-file',
-     type=str,
-     help='File to be decoded.'
-)
-
-read_parser.add_argument(
-     '-o',
-     type=str,
-     help='File to be decoded.'
-)
-
-
-cli_argument = vars(parser.parse_known_args()[0]).get('subparser_name')
-if cli_argument == 'read':
-     read_parser.set_defaults(func=CliArgumentParser.read(vars(read_parser.parse_known_args()[0])))
-elif cli_argument == 'write':
-     write_parser.set_defaults(func=CliArgumentParser.write(vars(write_parser.parse_known_args()[0])))
-
-# TODO:
+# This module is currently unused, but will remain for the future if there is a demand for CLI functionality.
+
+# import argparse
+#
+# from bitglitter.cli.analyzer import CliArgumentParser
+#
+# parser = argparse.ArgumentParser(description=""""Embed data payloads inside of ordinary
+#                                              images or video with high-performance 2-D barcodes.""")
+#
+#
+# subparsers = parser.add_subparsers(dest="subparser_name")
+#
+#
+# write_parser = subparsers.add_parser(
+#      'write',
+#      help="""This is the primary function in creating BitGlitter
+#      streams from files.  Please see Wiki page for more information.""",
+# )
+#
+# write_parser.add_argument(
+#      '-file',
+#      type=str,
+#      help='The file in which data will be written.'
+# )
+#
+# write_parser.add_argument(
+#      '-streamname',
+#      type=str,
+#      help='Stream name.'
+# )
+#
+# write_parser.add_argument(
+#      '-mode',
+#      type=str,
+#      help='File output mode: "image" or "video".'
+# )
+# write_parser.add_argument(
+#      '-o',
+#      type=str,
+#      help='File output path.'
+# )
+#
+#
+# # To implement here
+# read_parser = subparsers.add_parser(
+#      'read',
+#      help="""This is the high level function that decodes BitGlitter encoded images and video back into
+#                the files/folders contained within them.  This along with write() are the two primary
+#                functions of this library."""
+# )
+#
+# read_parser.add_argument(
+#      '-file',
+#      type=str,
+#      help='File to be decoded.'
+# )
+#
+# read_parser.add_argument(
+#      '-o',
+#      type=str,
+#      help='File to be decoded.'
+# )
+#
+#
+# cli_argument = vars(parser.parse_known_args()[0]).get('subparser_name')
+# if cli_argument == 'read':
+#      read_parser.set_defaults(func=CliArgumentParser.read(vars(read_parser.parse_known_args()[0])))
+# elif cli_argument == 'write':
+#      write_parser.set_defaults(func=CliArgumentParser.write(vars(write_parser.parse_known_args()[0])))
+
+#
 # parser.add_argument('--clearSession',
 #                     help='Tries to remove the session pickle if it exists, clearing all statistics and custom colors.')
 # parser.add_argument('--clearStats',
diff --git a/bitglitter/config/__init__.py b/bitglitter/config/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/bitglitter/config/config.py b/bitglitter/config/config.py
index db40060..105574a 100644
--- a/bitglitter/config/config.py
+++ b/bitglitter/config/config.py
@@ -1,11 +1,41 @@
-import pickle
+from sqlalchemy import Column, create_engine, Integer
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.orm import sessionmaker, scoped_session
+from sqlalchemy.pool import NullPool
 
-from bitglitter.config.configobjects import Config
+from pathlib import Path
 
-# Attempts to load previous session, otherwise it creates a few Config object.
-try:
-    with open ('config.pickle', 'rb') as pickleLoad:
-        config = pickle.load(pickleLoad)
+engine = create_engine(f'sqlite:///{Path(__file__).resolve().parent / "config.sqlite3"}?check_same_thread=False',
+                       poolclass=NullPool)
+engine.connect()
+Session = scoped_session(sessionmaker(bind=engine, expire_on_commit=False))
+session = Session()
+Base = declarative_base()
 
-except:
-    config = Config()
\ No newline at end of file
+
+class SQLBaseClass(Base):
+    """Removing duplicate boilerplate to make the code less cluttered, and the database objects themselves easier to
+    work with.
+    """
+
+    __abstract__ = True
+    id = Column(Integer, primary_key=True)
+    query = Session.query_property()
+
+    @classmethod
+    def create(cls, **kwargs):
+        object_ = cls(**kwargs)
+        session.add(object_)
+        session.commit()
+        return object_
+
+    def delete(self):
+        session.delete(self)
+        session.commit()
+
+    def save(self):
+        session.add(self)
+        session.commit()
+
+
+SQLBaseClass.metadata.create_all(engine)
diff --git a/bitglitter/config/configfunctions.py b/bitglitter/config/configfunctions.py
index b472b35..8353ebc 100644
--- a/bitglitter/config/configfunctions.py
+++ b/bitglitter/config/configfunctions.py
@@ -1,29 +1,77 @@
-import os
+from bitglitter.config.config import session
+from bitglitter.config.configmodels import Config, Constants, Statistics
+from bitglitter.config.defaultdbdata import load_default_db_data
+from bitglitter.config.palettemodels import Palette
+from bitglitter.config.presetmodels import Preset
+from bitglitter.config.readmodels.streamread import StreamRead
+from bitglitter.config.readmodels.readmodels import StreamDataProgress, StreamFile, StreamFrame, StreamSHA256Blacklist
 
-from bitglitter.config.config import config
 
-# These functions are for end users.
+def remove_session():
+    """Resets persistent data to factory default settings."""
+    model_list = [Config, Constants, Palette, Preset, Statistics, StreamDataProgress, StreamFile, StreamFrame,
+                  StreamRead, StreamSHA256Blacklist]
+    for model in model_list:
+        session.query(model).delete()
+    session.commit()
+    load_default_db_data()
 
-def output_stats(path):
-    '''Writes a text file to a file path outlining usage statistics.'''
 
-    active_path = os.path.join(os.getcwd(), path)
-    with open(active_path + '\\BitGlitter statistics.txt', 'w') as writer:
-        writer.write(str(config.stats_handler))
+def return_settings():
+    config = session.query(Config).first()
+    return {'read_path': config.read_path, 'read_bad_frame_strikes':
+            config.read_bad_frame_strikes, 'disable_bad_frame_strikes': config.enable_bad_frame_strikes, 'write_path':
+            config.write_path, 'log_txt_path': config.log_txt_dir, 'log_output': config.log_output,
+            'maximum_cpu_cores': config.maximum_cpu_cores, 'save_statistics': config.save_statistics,
+            'output_stream_title': config.output_stream_title, 'MAX_SUPPORTED_CPU_CORES':
+            config.MAX_SUPPORTED_CPU_CORES, 'logging_level': config.logging_level}
+
+
+def update_settings(read_path=None, read_bad_frame_strikes=None, disable_bad_frame_strikes=None,
+                    write_path=None, log_txt_path=None, log_output=None, logging_level=None, maximum_cpu_cores=None,
+                    save_statistics=None, output_stream_title=None):
+    config = session.query(Config).first()
+    if read_path:
+        config.read_path = read_path
+    if read_bad_frame_strikes:
+        config.read_bad_frame_strikes = read_bad_frame_strikes
+    if disable_bad_frame_strikes:
+        config.disable_bad_frame_strikes = disable_bad_frame_strikes
+    if write_path:
+        config.write_path = write_path
+    if log_txt_path:
+        config.log_txt_dir = log_txt_path
+    if log_output:
+        config.log_output = log_output
+    if config.logging_level:
+        config.logging_level = logging_level
+    if maximum_cpu_cores:
+        config.maximum_cpu_cores = maximum_cpu_cores
+    if save_statistics:
+        config.save_statistics = save_statistics
+    if output_stream_title:
+        config.output_stream_title = output_stream_title
+    config.save()
+
+
+def output_stats():
+    """Returns a dictionary object containing read and write statistics."""
+    stats = session.query(Statistics).first()
+    return stats.return_stats()
 
 
 def clear_stats():
-    '''Resets statistics back to zero in all fields.'''
+    """Resets all write and read values back to zero."""
+    stats = session.query(Statistics).first()
+    stats.clear_stats()
 
-    config.stats_handler.clear_stats()
-    config.save_session()
 
+def write_stats_update(blocks, frames, data):
+    """Internal function to update stats after rendering completes, along with read update below."""
+    stats = session.query(Statistics).first()
+    stats.write_update(blocks, frames, data)
 
-def clear_session():
-    '''Tries to remove the session pickle if it exists, clearing all statistics and custom colors.'''
 
-    try:
-        config.assembler.clear_partial_saves()
-        os.remove('config.pickle')
-    except:
-        pass
+def read_stats_update(blocks, frames, data):
+    stats = session.query(Statistics).first()
+    stats.read_update(blocks, frames, data)
diff --git a/bitglitter/config/configmodels.py b/bitglitter/config/configmodels.py
new file mode 100644
index 0000000..1d9e54d
--- /dev/null
+++ b/bitglitter/config/configmodels.py
@@ -0,0 +1,131 @@
+from sqlalchemy import Boolean, Column, Integer, String
+
+from multiprocessing import cpu_count
+from pathlib import Path
+
+from bitglitter.config.config import engine, session, SQLBaseClass
+
+
+class Config(SQLBaseClass):
+    __abstract__ = False
+    __tablename__ = 'config'
+    read_path = Column(String, default=str(Path(__file__).resolve().parent.parent / 'Read Output'))
+    read_bad_frame_strikes = Column(Integer, default=10)
+    enable_bad_frame_strikes = Column(Boolean, default=True)
+    write_path = Column(String, default=str(Path(__file__).resolve().parent.parent / 'Write Output'))
+    log_txt_dir = Column(String, default=str(Path(__file__).resolve().parent.parent / 'Logs'))
+    log_output = Column(Boolean, default=False)
+    logging_level = Column(Integer, default=1)
+    maximum_cpu_cores = Column(Integer, default=cpu_count())
+    MAX_SUPPORTED_CPU_CORES = Column(Integer, default=cpu_count())
+    save_statistics = Column(Boolean, default=True)
+    output_stream_title = Column(Boolean, default=True)  # App version
+
+
+class Constants(SQLBaseClass):
+    __abstract__ = False
+    __tablename__ = 'constants'
+    BG_VERSION = Column(String, default='2.0', nullable=False)  # Change as needed
+    PROTOCOL_VERSION = Column(Integer, default=1, nullable=False)
+    SUPPORTED_PROTOCOLS = Column(String, default='1', nullable=False)
+
+    WORKING_DIR = Column(String, default=str(Path(__file__).resolve().parent.parent / 'Temp'), nullable=False)
+    DEFAULT_WRITE_DIR = Column(String, default=str(Path(__file__).resolve().parent.parent / 'Write Output'),
+                               nullable=False)
+    DEFAULT_READ_DIR = Column(String, default=str(Path(__file__).resolve().parent.parent / 'Read Output'),
+                              nullable=False)
+
+    VALID_VIDEO_FORMATS = Column(String, default='.avi|.flv|.mov|.mp4|.wmv', nullable=False)
+    VALID_IMAGE_FORMATS = Column(String, default='.bmp|.jpg|.jpeg|.png|.webp', nullable=False)
+
+    def return_supported_protocols(self):
+        return self.SUPPORTED_PROTOCOLS.split('|')
+
+    def return_valid_video_formats(self):
+        return self.VALID_VIDEO_FORMATS.split('|')
+
+    def return_valid_image_formats(self, glob_format=False):
+        if not glob_format:
+            return self.VALID_IMAGE_FORMATS.split('|')
+        else:
+            format_list = self.VALID_IMAGE_FORMATS.split('|')
+            return [item.replace('.', '*.') for item in format_list]
+
+
+class Statistics(SQLBaseClass):
+    __abstract__ = False
+    __tablename__ = 'statistics'
+    blocks_wrote = Column(Integer, default=0)
+    frames_wrote = Column(Integer, default=0)
+    data_wrote_bits = Column(Integer, default=0)
+    blocks_read = Column(Integer, default=0)
+    frames_read = Column(Integer, default=0)
+    data_read_bits = Column(Integer, default=0)
+
+    def write_update(self, blocks, frames, data):
+        self.blocks_wrote += blocks
+        self.frames_wrote += frames
+        self.data_wrote_bits += data
+        self.save()
+
+    def read_update(self, blocks, frames, data):
+        self.blocks_read += blocks
+        self.frames_read += frames
+        self.data_read_bits += data
+        self.save()
+
+    def return_stats(self):
+        return {
+            'blocks_wrote': round(self.data_wrote_bits / 8), 'frames_wrote': self.frames_wrote, 'data_wrote':
+                self.data_wrote_bits, 'blocks_read': self.blocks_read, 'frames_read': self.frames_read, 'data_read':
+                round(self.data_read_bits),
+        }
+
+    def clear_stats(self):
+        self.blocks_wrote = 0
+        self.frames_wrote = 0
+        self.data_wrote_bits = 0
+        self.blocks_read = 0
+        self.frames_read = 0
+        self.data_read_bits = 0
+        self.save()
+
+
+class CurrentJobState(SQLBaseClass):
+    """Lightweight singleton object that is queried for every frame read or written when ran with Electron app, to
+    indicate if the current job has been cancelled.  This runs at the beginning of each frame.
+    """
+
+    __abstract__ = False
+    __tablename__ = 'current_job_state'
+
+    active_stream_sha256 = Column(String)
+    is_cancelled = Column(Boolean, default=False)
+
+    @classmethod
+    def new_task(cls, stream_sha256):
+        singleton = session.query(CurrentJobState).first()
+        singleton.is_cancelled = False
+        singleton.active_stream_sha256 = stream_sha256
+        singleton.save()
+
+    @classmethod
+    def check_state(cls):
+        singleton = session.query(CurrentJobState).first()
+        return singleton.active_stream_sha256, singleton.is_cancelled
+
+    @classmethod
+    def cancel(cls):
+        singleton = session.query(CurrentJobState).first()
+        singleton.is_cancelled = True
+        singleton.save()
+
+    @classmethod
+    def end_task(cls):
+        singleton = session.query(CurrentJobState).first()
+        singleton.active_stream_sha256 = None
+        singleton.is_cancelled = False
+        singleton.save()
+
+
+SQLBaseClass.metadata.create_all(engine)
diff --git a/bitglitter/config/configobjects.py b/bitglitter/config/configobjects.py
deleted file mode 100644
index 78d8e50..0000000
--- a/bitglitter/config/configobjects.py
+++ /dev/null
@@ -1,119 +0,0 @@
-import pickle
-
-from bitglitter.palettes.paletteobjects import DefaultPalette, TwentyFourBitPalette
-from bitglitter.read.assembler import Assembler
-
-
-class Config:
-    '''This is the master object that holds all session data.'''
-
-    def __init__(self):
-
-        self.color_handler = PaletteHandler()
-        self.stats_handler = Statistics()
-        self.assembler = Assembler()
-        self.assembler.clear_partial_saves() # Deleting old folder if new config object must be made.
-
-        self.save_session()
-
-    # Reserved for next release, introducing presets
-    # preset_dict = {}
-
-    def save_session(self):
-        with open('config.pickle', 'wb') as pickleSaver:
-            pickle.dump(self, pickleSaver)
-
-
-class Statistics:
-    '''Read and write values are held in this object.  It's attributes are changed through method calls.'''
-
-    def __init__(self):
-
-        self.blocks_wrote = 0
-        self.frames_wrote = 0
-        self.data_wrote = 0
-
-        self.blocks_read = 0
-        self.frames_read = 0
-        self.data_read = 0
-
-    def __str__(self):
-        '''This is used by output_stats() in configfunctions to output a nice formatted text file showing usage
-        statistics.
-        '''
-
-        return('*' * 21 + '\nStatistics\n' + '*' * 21 + f'\n\nTotal Blocks Wrote: {self.blocks_wrote}'
-                                                                f'\nTotal Frames Wrote: {self.frames_wrote}'
-                                                                f'\nTotal Data Wrote: {int(self.data_wrote / 8)} B'
-                                                                f'\n\nTotal Blocks Read: {self.blocks_read}'
-                                                                f'\nTotal Frames Read: {self.frames_read}'
-                                                                f'\nTotal Data Read: {int(self.data_read / 8)} B')
-
-    def write_update(self, blocks, frames, data):
-        self.blocks_wrote += blocks
-        self.frames_wrote += frames
-        self.data_wrote += data
-
-
-    def read_update(self, blocks, frames, data):
-        '''Deprecated.  May be removed.'''
-
-        self.blocks_read += blocks
-        self.frames_read += frames
-        self.data_read += data
-
-    def clear_stats(self):
-        self.blocks_wrote = 0
-        self.frames_wrote = 0
-        self.data_wrote = 0
-        self.blocks_read = 0
-        self.frames_read = 0
-        self.data_read = 0
-
-
-class PaletteHandler:
-    '''This handles all palettes both default and custom.  Please note that default palettes are created here as well.
-    All functions available in palettefunctions module that deal with custom palettes are interfacing with dictionaries
-    custom_palette_list and custom_palette_nickname_list in this object.
-    '''
-
-    def __init__(self):
-        self.default_palette_list = {'1' : DefaultPalette("1 bit default",
-                            "Two colors, black and white.  While it has the lowest density of one bit of data per "
-                            "pixel, it has the highest reliability.", ((0,0,0), (255,255,255)), 441.67, 1),
-
-                            '11' : DefaultPalette("1 bit alternate", "Uses cyan/magenta instead of white/black.",
-                            ((255, 0, 255), (0, 255, 255)), 360.12, 11),
-
-                            '2' : DefaultPalette("2 bit default", "Four colors; black, red, green, blue.",
-                            ((0,0,0), (255,0,0), (0,255,0), (0,0,255)), 255, 2),
-
-                            '22': DefaultPalette("2 bit alternate", "Four colors; black, magenta, cyan, yellow.",
-                            ((0, 0, 0), (255, 255, 0), (0, 255, 255), (255, 0, 255)), 255, 22),
-
-                            '3' : DefaultPalette("3 bit default",
-                            "Eight colors.", ((0,0,0), (255,0,0), (0,255,0), (0,0,255), (255,255,0), (0,255,255),
-                            (255,0,255), (255,255,255)), 255, 3),
-
-                            '4' : DefaultPalette("4 bit default", "Sixteen colors.", ((0,0,0), (128,128,128),
-                            (192,192,192), (128,0,0), (255,0,0), (128,128,0), (255,255,0), (0,255,0), (0,128,128),
-                            (0,128,0), (0,0,128), (0,0,255), (0,255,255), (128,0,128), (255,0,255), (255,255,255)),
-                                                        109.12, 4),
-
-                            '6' : DefaultPalette("6 bit default", "Sixty-four colors.", ((0,0,0), (0,0,85),
-                            (0,0,170), (0,0,255), (0,85,0), (0,85,85), (0,85,170), (0,85,255), (0,170,0), (0,170,85),
-                            (0,170,170), (0,170,255), (0,255,0), (0,255,85), (0,255,170), (0,255,255), (85,0,0),
-                            (85,0,85), (85,0,170), (85,0,255), (85,85,0), (85,85,85), (85,85,170), (85,85,255),
-                            (85,170,0), (85,170,85), (85,170,170), (85,170,255), (85,255,0), (85,255,85), (85,255,170),
-                            (85,255,255), (170,0,0), (170,0,85), (170,0,170), (170,0,255), (170,85,0), (170,85,85),
-                            (170,85,170), (170,85,255), (170,170,0), (170,170,85), (170,170,170), (170,170,255),
-                            (170,255,0), (170,255,85), (170,255,170), (170,255,255), (255,0,0), (255,0,85), (255,0,170),
-                            (255,0,255), (255,85,0), (255,85,85), (255,85,170), (255,85,255), (255,170,0), (255,170,85),
-                            (255,170,170), (255,170,255), (255,255,0), (255,255,85), (255,255,170), (255,255,255)), 85,
-                            6),
-
-                            '24': TwentyFourBitPalette()
-                            }
-
-        self.custom_palette_list = {}
-        self.custom_palette_nickname_list = {}
\ No newline at end of file
diff --git a/bitglitter/config/constants.py b/bitglitter/config/constants.py
deleted file mode 100644
index 02c9d9f..0000000
--- a/bitglitter/config/constants.py
+++ /dev/null
@@ -1,27 +0,0 @@
-# This gets changed every new version.  It is included in the ASCII part of the stream header for diagnostic and
-# debugging purposes.
-
-# Dev note- this gets updated at version change.  This is included in ASCII stream header for debugging purposes.
-BG_VERSION = "1.0"
-
-# Default folder paths
-WRITE_PATH = 'Temp'
-READ_PATH = 'Partial Saves'
-
-# Crypto
-SCRYPT_N_DEFAULT = 14
-SCRYPT_R_DEFAULT = 8
-SCRYPT_P_DEFAULT = 1
-
-# Read specific
-BAD_FRAME_STRIKES = 10
-VALID_VIDEO_FORMATS = ['.avi', '.flv', '.mov', '.mp4', '.wmv']
-VALID_IMAGE_FORMATS = ['.bmp', '.jpg', '.png']
-
-# Write specific
-HEADER_PALETTE_ID ='6'
-STREAM_PALETTE_ID ='6'
-PIXEL_WIDTH = 24
-BLOCK_HEIGHT = 45
-BLOCK_WIDTH = 80
-FRAMES_PER_SECOND = 30
\ No newline at end of file
diff --git a/bitglitter/config/defaultdbdata.py b/bitglitter/config/defaultdbdata.py
new file mode 100644
index 0000000..5dd6ad4
--- /dev/null
+++ b/bitglitter/config/defaultdbdata.py
@@ -0,0 +1,490 @@
+from bitglitter.config.config import session
+from bitglitter.config.configmodels import Config, Constants, CurrentJobState, Statistics
+from bitglitter.config.palettemodels import Palette
+
+
+def load_default_db_data():
+    if session.query(Config).first():
+        return
+
+    Constants.create()
+    Statistics.create()
+    Config.create()
+    CurrentJobState.create()
+
+    # Palette create
+    default_palette_data = [
+        {
+            'palette_id': '1',
+            'nickname': '1',
+            'name': '1 Bit Default',
+            'description': 'Two colors, black and white.  While it has the lowest density of one bit of data per pixel,'
+                           ' it has the highest reliability.  This palette is used in all initial headers, displaying'
+                           ' core stream metadata.',
+            'color_set': ((0, 0, 0), (255, 255, 255)),
+            'is_24_bit': False,
+            'is_custom': False
+        },
+        {
+            'palette_id': '11',
+            'nickname': '11',
+            'name': '1 Bit Default Alternate',
+            'description': 'Alternate version.  Uses cyan/magenta instead of white/black.  This palette is used in the'
+                           ' vertical and horizontal calibration blocks seen in all image frames, and the first frame'
+                           ' of videos.',
+            'color_set': ((255, 0, 255), (0, 255, 255)),
+            'is_24_bit': False,
+            'is_custom': False
+        },
+        {
+            'palette_id': '2',
+            'nickname': '2',
+            'name': '2 Bit Default',
+            'description': 'Four colors- black, red, green, blue.',
+            'color_set': ((0, 0, 0), (255, 0, 0), (0, 255, 0), (0, 0, 255)),
+            'is_24_bit': False,
+            'is_custom': False
+        },
+        {
+            'palette_id': '22',
+            'nickname': '22',
+            'name': '2 Bit Default Alternate',
+            'description': 'Alternate version.  Four colors- black, magenta, cyan, yellow.',
+            'color_set': ((0, 0, 0), (255, 255, 0), (0, 255, 255), (255, 0, 255)),
+            'is_24_bit': False,
+            'is_custom': False
+        },
+        {
+            'palette_id': '3',
+            'nickname': '3',
+            'name': '3 Bit Default',
+            'description': 'Eight colors.',
+            'color_set': ((0, 0, 0), (255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 0), (0, 255, 255), (255, 0, 255),
+                          (255, 255, 255)),
+            'is_24_bit': False,
+            'is_custom': False
+        },
+        {
+            'palette_id': '4',
+            'nickname': '4',
+            'name': '4 Bit Default',
+            'description': 'Sixteen colors.',
+            'color_set': ((0, 0, 0), (128, 128, 128), (192, 192, 192), (128, 0, 0), (255, 0, 0), (128, 128, 0), (255,
+                                                                                                                 255,
+                                                                                                                 0),
+                          (0, 255, 0), (0, 128, 128), (0, 128, 0), (0, 0, 128), (0, 0, 255), (0, 255, 255),
+                          (128, 0, 128), (255, 0, 255), (255, 255, 255)),
+            'is_24_bit': False,
+            'is_custom': False
+        },
+        {
+            'palette_id': '6',
+            'nickname': '6',
+            'name': '6 Bit Default',
+            'description': 'Sixty-four colors.  This is the default palette used for BitGlitter because it has good '
+                           'performance while still quite resistant to typical compression/distortion.',
+            'color_set': ((0, 0, 0), (0, 0, 85), (0, 0, 170), (0, 0, 255), (0, 85, 0), (0, 85, 85), (0, 85, 170),
+                          (0, 85, 255), (0, 170, 0), (0, 170, 85), (0, 170, 170), (0, 170, 255), (0, 255, 0),
+                          (0, 255, 85), (0, 255, 170), (0, 255, 255), (85, 0, 0), (85, 0, 85), (85, 0, 170),
+                          (85, 0, 255), (85, 85, 0), (85, 85, 85), (85, 85, 170), (85, 85, 255), (85, 170, 0),
+                          (85, 170, 85), (85, 170, 170), (85, 170, 255), (85, 255, 0), (85, 255, 85), (85, 255, 170),
+                          (85, 255, 255), (170, 0, 0), (170, 0, 85), (170, 0, 170), (170, 0, 255), (170, 85, 0),
+                          (170, 85, 85), (170, 85, 170), (170, 85, 255), (170, 170, 0), (170, 170, 85), (170, 170, 170),
+                          (170, 170, 255), (170, 255, 0), (170, 255, 85), (170, 255, 170), (170, 255, 255), (255, 0, 0),
+                          (255, 0, 85), (255, 0, 170), (255, 0, 255), (255, 85, 0), (255, 85, 85), (255, 85, 170),
+                          (255, 85, 255), (255, 170, 0), (255, 170, 85), (255, 170, 170), (255, 170, 255),
+                          (255, 255, 0), (255, 255, 85), (255, 255, 170), (255, 255, 255)),
+            'is_24_bit': False,
+            'is_custom': False
+        },
+        {
+            'palette_id': '24',
+            'nickname': '24',
+            'name': '24 Bit Default',
+            'description': '~16.7 million colors, the best performing palette possible by a factor of three (with a '
+                           'very big if).  This only works in lossless environments, any sort of distortion to the'
+                           ' rendered file WILL corrupt the data!  There is zero margin for error.\n\nOnly currently'
+                           ' works in rendering images as its a semi-experimental palette.',
+            'color_set': None,
+            'is_24_bit': True,
+            'is_custom': False
+        },
+    ]
+    custom_palette_data = [
+        {
+            'palette_id': '',
+            'nickname': 'paperback2',
+            'name': 'Paperback-2',
+            'description': 'Two colors.  Credit goes to Doph of lospec.com.',
+            'color_set': ('b8c2b9', '382b26'),
+            'is_24_bit': False,
+            'is_custom': True
+        },
+        {
+            'palette_id': '',
+            'nickname': 'pixelink',
+            'name': 'Pixel Ink',
+            'description': 'Two colors.  Credit goes to Polyducks of lospec.com.',
+            'color_set': ('3e232c', 'edf6d6'),
+            'is_24_bit': False,
+            'is_custom': True
+        },
+        {
+            'palette_id': '',
+            'nickname': 'knockia',
+            'name': 'Knockia3310',
+            'description': 'Two colors.  Credit goes to Imogia Games of lospec.com.',
+            'color_set': ('212c28', '72a488'),
+            'is_24_bit': False,
+            'is_custom': True
+        },
+        {
+            'palette_id': '',
+            'nickname': 'note2c',
+            'name': 'Note-2C',
+            'description': 'Two colors.  Credit goes to Rytzi of lospec.com.',
+            'color_set': ('222a3d', 'edf2e2'),
+            'is_24_bit': False,
+            'is_custom': True
+        },
+        {
+            'palette_id': '',
+            'nickname': 'funkyjam',
+            'name': 'Y\'s Funky Jam',
+            'description': 'Two colors.  Credit goes to Yelta of lospec.com.',
+            'color_set': ('920244', 'fec28c'),
+            'is_24_bit': False,
+            'is_custom': True
+        },
+        {
+            'palette_id': '',
+            'nickname': 'kirokaze',
+            'name': 'Kirokaze Gameboy Palette',
+            'description': 'Four colors.  Credit goes to Kirokaze of lospec.com.',
+            'color_set': ('332c50', '46878f', '94e344', 'e2f3e4'),
+            'is_24_bit': False,
+            'is_custom': True
+        },
+        {
+            'palette_id': '',
+            'nickname': 'icecream',
+            'name': 'Ice Cream GB',
+            'description': 'Four colors.  Credit goes to Kerrie Lake of lospec.com.',
+            'color_set': ('7c3f58', 'eb6b6f', 'f9a875', 'fff6d3'),
+            'is_24_bit': False,
+            'is_custom': True
+        },
+        {
+            'palette_id': '',
+            'nickname': '2bitgrayscale',
+            'name': '2-Bit Grayscale',
+            'description': 'Four colors.  Credit goes to lospec.com.',
+            'color_set': ('000000', '676767', 'b6b6b6', 'ffffff'),
+            'is_24_bit': False,
+            'is_custom': True
+        },
+        {
+            'palette_id': '',
+            'nickname': 'mist',
+            'name': 'Mist GB',
+            'description': 'Four colors.  Credit goes to Kerrie Lake of lospec.com.',
+            'color_set': ('2d1b00', '1e606e', '5ab9a8', 'c4f0c2'),
+            'is_24_bit': False,
+            'is_custom': True
+        },
+        {
+            'palette_id': '',
+            'nickname': 'rustic',
+            'name': 'Rustic GB',
+            'description': 'Four colors.  Credit goes to Kerrie Lake of lospec.com.',
+            'color_set': ('2c2137', '764462', 'edb4a1', 'a96868'),
+            'is_24_bit': False,
+            'is_custom': True
+        },
+        {
+            'palette_id': '',
+            'nickname': 'spacehaze',
+            'name': 'SpaceHaze',
+            'description': 'Four colors.  Credit goes to WildLeoKnight of lospec.com.',
+            'color_set': ('f8e3c4', 'cc3495', '6b1fb1', '0b0630'),
+            'is_24_bit': False,
+            'is_custom': True
+        },
+        {
+            'palette_id': '',
+            'nickname': 'slso8',
+            'name': 'SLSO8',
+            'description': 'Eight colors.  Credit goes to Luis Miguel Maldonado of lospec.com.',
+            'color_set': ('0d2b45', '203c56', '544e68', '8d697a', 'd08159', 'ffaa5e', 'ffd4a3', 'ffecd6'),
+            'is_24_bit': False,
+            'is_custom': True
+        },
+        {
+            'palette_id': '',
+            'nickname': 'dreamscape8',
+            'name': 'Dreamscape8',
+            'description': 'Eight colors.  Credit goes to Kerrie Lake of lospec.com.',
+            'color_set': ('c9cca1', 'caa05a', 'ae6a47', '8b4049', '543344', '515262', '63787d', '8ea091'),
+            'is_24_bit': False,
+            'is_custom': True
+        },
+        {
+            'palette_id': '',
+            'nickname': 'pollen8',
+            'name': 'Pollen8',
+            'description': 'Eight colors.  Credit goes to Conker of lospec.com.',
+            'color_set': ('73464c', 'ab5675', 'ee6a7c', 'ffa7a5', 'ffe07e', 'ffe7d6', '72dcbb', '34acba'),
+            'is_24_bit': False,
+            'is_custom': True
+        },
+        {
+            'palette_id': '',
+            'nickname': 'ammo8',
+            'name': 'Ammo-8',
+            'description': 'Eight colors.  Credit goes to rsvp asap of lospec.com.',
+            'color_set': ('040c06', '112318', '1e3a29', '305d42', '4d8061', '89a257', 'bedc7f', 'eeffcc'),
+            'is_24_bit': False,
+            'is_custom': True
+        },
+        {
+            'palette_id': '',
+            'nickname': 'funkyfuture8',
+            'name': 'FunkyFuture 8',
+            'description': 'Eight colors.  Credit goes to Shamaboy of lospec.com.',
+            'color_set': ('2b0f54', 'ab1f65', 'ff4f69', 'fff7f8', 'ff8142', 'ffda45', '3368dc', '49e7ec'),
+            'is_24_bit': False,
+            'is_custom': True
+        },
+        {
+            'palette_id': '',
+            'nickname': 'justparchment8',
+            'name': 'JustParchment 8',
+            'description': 'Eight colors.  Credit goes to JustJimmy of lospec.com.',
+            'color_set': ('292418', '524839', '73654a', '8b7d62', 'a48d6a', 'bda583', 'cdba94', 'e6ceac'),
+            'is_24_bit': False,
+            'is_custom': True
+        },
+        {
+            'palette_id': '',
+            'nickname': 'rustgold',
+            'name': 'Rust Gold 8',
+            'description': 'Eight colors.  Credit goes to Trigo Mathmancer of lospec.com.',
+            'color_set': ('f6cd26', 'ac6b26', '563226', '331c17', 'bb7f57', '725956', '393939', '202020'),
+            'is_24_bit': False,
+            'is_custom': True
+        },
+        {
+            'palette_id': '',
+            'nickname': 'oddfeeling',
+            'name': 'Odd Feeling',
+            'description': 'Eight colors.  Credit goes to XENO of lospec.com.',
+            'color_set': ('900c3f', 'e84a5f', 'ff847c', 'fc9d9d', 'feceab', 'ccafaf', '99b898', 'ffffff'),
+            'is_24_bit': False,
+            'is_custom': True
+        },
+        {
+            'palette_id': '',
+            'nickname': 'retrocal',
+            'name': 'RetroCal-8',
+            'description': 'Eight colors.  Credit goes to polyphorge of lospec.com.',
+            'color_set': ('6eb8a8', '2a584f', '74a33f', 'fcffc0', 'c6505a', '2f142f', '774448', 'ee9c5d'),
+            'is_24_bit': False,
+            'is_custom': True
+        },
+        {
+            'palette_id': '',
+            'nickname': 'citrink',
+            'name': 'Citrink',
+            'description': 'Eight colors.  Credit goes to Inkpendude of lospec.com.',
+            'color_set': ('ffffff', 'fcf660', 'b2d942', '52c33f', '166e7a', '254d70', '252446', '201533'),
+            'is_24_bit': False,
+            'is_custom': True
+        },
+        {
+            'palette_id': '',
+            'nickname': 'waverator',
+            'name': 'Waverator',
+            'description': 'Eight colors.  Credit goes to Mig Moog of lospec.com.',
+            'color_set': ('0c0d14', '181c28', '23313d', '33505d', '4e7f7d', '53a788', '70d38b', 'cbffd8'),
+            'is_24_bit': False,
+            'is_custom': True
+        },
+        {
+            'palette_id': '',
+            'nickname': 'winterwonderland',
+            'name': 'Winter Wonderland',
+            'description': 'Eight colors.  Credit goes to Jimison3 of lospec.com.',
+            'color_set': ('20284e', '2c4a78', '3875a1', '8bcadd', 'ffffff', 'd6e1e9', 'a7bcc9', '738d9d'),
+            'is_24_bit': False,
+            'is_custom': True
+        },
+        {
+            'palette_id': '',
+            'nickname': 'sobeachy',
+            'name': 'SoBeachy8',
+            'description': 'Eight colors.  Credit goes to Snowy Owl of lospec.com.',
+            'color_set': ('e55388', 'e57d88', 'e59f88', 'e5d988', 'e3d5cc', 'bad5cc', '6dd5cc', '5ac5cc'),
+            'is_24_bit': False,
+            'is_custom': True
+        },
+        {
+            'palette_id': '',
+            'nickname': 'sweetie',
+            'name': 'Sweetie 16',
+            'description': 'Sixteen colors.  Credit goes to GrafxKid of lospec.com.',
+            'color_set': ('1a1c2c', '5d275d', 'b13e53', 'ef7d57', 'ffcd75', 'a7f070', '38b764', '257179', '29366f',
+                          '3b5dc9', '41a6f6', '73eff7', 'f4f4f4', '94b0c2', '566c86', '333c57'),
+            'is_24_bit': False,
+            'is_custom': True
+        },
+        {
+            'palette_id': '',
+            'nickname': 'steamlords',
+            'name': 'Steam Lords',
+            'description': 'Sixteen colors.  Credit goes to Slynyrd of lospec.com.',
+            'color_set': ('213b25', '3a604a', '4f7754', 'a19f7c', '77744f', '775c4f', '603b3a', '3b2137', '170e19',
+                          '2f213b', '433a60', '4f5277', '65738c', '7c94a1', 'a0b9ba', 'c0d1cc'),
+            'is_24_bit': False,
+            'is_custom': True
+        },
+        {
+            'palette_id': '',
+            'nickname': 'islandjoy',
+            'name': 'Island Joy 16',
+            'description': 'Sixteen colors.  Credit goes to Kerrie Lake of lospec.com.',
+            'color_set': ('ffffff', '6df7c1', '11adc1', '606c81', '393457', '1e8875', '5bb361', 'a1e55a', 'f7e476',
+                          'f99252', 'cb4d68', '6a3771', 'c92464', 'f48cb6', 'f7b69e', '9b9c82'),
+            'is_24_bit': False,
+            'is_custom': True
+        },
+        {
+            'palette_id': '',
+            'nickname': 'lostcentury',
+            'name': 'Lost Century',
+            'description': 'Sixteen colors.  Credit goes to CalmRadish of lospec.com.',
+            'color_set': ('d1b187', 'c77b58', 'ae5d40', '79444a', '4b3d44', 'ba9158', '927441', '4d4539', '77743b',
+                          'b3a555', 'd2c9a5', '8caba1', '4b726e', '574852', '847875', 'ab9b8e'),
+            'is_24_bit': False,
+            'is_custom': True
+        },
+        {
+            'palette_id': '',
+            'nickname': 'vanillamilkshake',
+            'name': 'Vanilla Milkshake',
+            'description': 'Sixteen colors.  Credit goes to Space Sandwich of lospec.com.',
+            'color_set': ('28282e', '6c5671', 'd9c8bf', 'f98284', 'b0a9e4', 'accce4', 'b3e3da', 'feaae4', '87a889',
+                          'b0eb93', 'ffe6c6', 'dea38b', 'e9f59d', 'ffc384', 'fff7a0', 'fff7e4'),
+            'is_24_bit': False,
+            'is_custom': True
+        },
+        {
+            'palette_id': '',
+            'nickname': 'galaxyflame',
+            'name': 'Galaxy Flame',
+            'description': 'Sixteen colors.  Credit goes to Rhoq of lospec.com.',
+            'color_set': ('699fad', '3a708e', '2b454f', '111215', '151d1a', '1d3230', '314e3f', '4f5d42', '9a9f87',
+                          'ede6cb', 'f5d893', 'e8b26f', 'b6834c', '704d2b', '40231e', '151015'),
+            'is_24_bit': False,
+            'is_custom': True
+        },
+        {
+            'palette_id': '',
+            'nickname': 'endesga32',
+            'name': 'Endesga 32',
+            'description': 'Thirty-two colors.  Credit goes to ENDESGA of lospec.com.',
+            'color_set': ('be4a2f', 'd77643', 'ead4aa', 'e4a672', 'b86f50', '733e39', '3e2731', 'a22633', 'e43b44',
+                          'f77622', 'feae34', 'fee761', '63c74d', '3e8948', '265c42', '193c3e', '124e89', '0099db',
+                          '2ce8f5', 'ffffff', 'c0cbdc', '8b9bb4', '5a6988', '3a4466', '262b44', '181425', 'ff0044',
+                          '68386c', 'b55088', 'f6757a', 'e8b796', 'c28569'),
+            'is_24_bit': False,
+            'is_custom': True
+        },
+        {
+            'palette_id': '',
+            'nickname': 'mulfok32',
+            'name': 'MulfoK32',
+            'description': 'Thirty-two colors.  Credit goes to MulfoK of lospec.com.',
+            'color_set': ('5ba675', '6bc96c', 'abdd64', 'fcef8d', 'ffb879', 'ea6262', 'cc425e', 'a32858', '751756',
+                          '390947', '611851', '873555', 'a6555f', 'c97373', 'f2ae99', 'ffc3f2', 'ee8fcb', 'd46eb3',
+                          '873e84', '1f102a', '4a3052', '7b5480', 'a6859f', 'd9bdc8', 'ffffff', 'aee2ff', '8db7ff',
+                          '6d80fa', '8465ec', '834dc4', '7d2da0', '4e187c'),
+            'is_24_bit': False,
+            'is_custom': True
+        },
+        {
+            'palette_id': '',
+            'nickname': 'warm32',
+            'name': 'Warm32',
+            'description': 'Thirty-two colors.  Credit goes to David of lospec.com.',
+            'color_set': ('0d0e1e', '2f3144', '626a73', '94a5aa', 'd3dfe1', '291820', '694749', 'a56e66', 'cb9670',
+                          'ecd8b7', '28092d', '692b58', '804061', 'a1516a', 'e19393', '1e1d38', '514569', '84788b',
+                          'bea8bf', '232d4f', '3a4b6d', '65799a', '99b4dd', '41648b', '6fa9c3', 'b9e2e5', 'd3ead8',
+                          '0a2325', '204039', '3e6248', '778f73', 'b4c3a8'),
+            'is_24_bit': False,
+            'is_custom': True
+        },
+        {
+            'palette_id': '',
+            'nickname': 'res64',
+            'name': 'Resurrect64',
+            'description': 'Sixty-four colors.  Credit goes to Kerrie Lake of lospec.com.',
+            'color_set': ('2e222f', '3e3546', '625565', '966c6c', 'ab947a', '694f62', '7f708a', '9babb2', 'c7dcd0',
+                          'ffffff', '6e2727', 'b33831', 'ea4f36', 'f57d4a', 'ae2334', 'e83b3b', 'fb6b1d', 'f79617',
+                          'f9c22b', '7a3045', '9e4539', 'cd683d', 'e6904e', 'fbb954', '4c3e24', '676633', 'a2a947',
+                          'd5e04b', 'fbff86', '165a4c', '239063', '1ebc73', '91db69', 'cddf6c', '313638', '374e4a',
+                          '547e64', '92a984', 'b2ba90', '0b5e65', '0b8a8f', '0eaf9b', '30e1b9', '8ff8e2', '323353',
+                          '484a77', '4d65b4', '4d9be6', '8fd3ff', '45293f', '6b3e75', '905ea9', 'a884f3', 'eaaded',
+                          '753c54', 'a24b6f', 'cf657f', 'ed8099', '831c5d', 'c32454', 'f04f78', 'f68181', 'fca790',
+                          'fdcbb0'),
+
+            'is_24_bit': False,
+            'is_custom': True
+        },
+        {
+            'palette_id': '',
+            'nickname': 'nanner',
+            'name': 'Nanner Pancakes',
+            'description': 'Thirty-two colors.  Credit goes to Nanner of lospec.com.',
+            'color_set': ('a0ddd3', '6fb0b7', '577f9d', '4a5786', '3e3b66', '392945', '2d1e2f', '452e3f', '5d4550',
+                          '7b6268', '9c807e', 'c3a79c', 'dbc9b4', 'fcecd1', 'aad795', '64b082', '488885', '3f5b74',
+                          'ebc8a7', 'd3a084', 'b87e6c', '8f5252', '6a3948', 'c57f79', 'ab597d', '7c3d64', '4e2b45',
+                          '7a3b4f', 'a94b54', 'd8725e', 'f09f71', 'f7cf91'),
+            'is_24_bit': False,
+            'is_custom': True
+        },
+        {
+            'palette_id': '',
+            'nickname': 'stardust32',
+            'name': 'Stardust32',
+            'description': 'Thirty-two colors.  Credit goes to Isa of lospec.com.',
+            'color_set': ('000833', '200261', '330a80', '851694', 'd42a6e', 'ff5b4f', 'ffb366', 'fff673', 'aff04f',
+                          '48d43b', '1aab87', '2e51c7', '2320ab', '3f87d9', '54b8e3', '78f0e2', 'b8ffd9', 'ffffe1',
+                          'ffa8bf', 'e681ac', 'bd448e', 'ffdac9', 'eba698', 'cc6e6e', 'a14869', '80335e', '521d45',
+                          '2b1744', '54508a', '6f75a6', '91a2c2', 'cadbe8'),
+            'is_24_bit': False,
+            'is_custom': True
+        },
+        {
+            'palette_id': '',
+            'nickname': 'pastel64',
+            'name': 'Pastel-64',
+            'description': 'Sixty-four colors.  Credit goes to helixei of lospec.com.',
+            'color_set': ('998276', 'c4c484', 'abd883', 'a2f2bd', 'a2ebf2', 'b88488', 'd1b182', 'd4eb91', 'ccfcc4',
+                          '907699', 'c484a4', 'ea8c79', 'f2e5a2', '9a84b8', 'd182ca', 'eb91a8', 'ffddc4', '768d99',
+                          '8484c4', 'c479ea', 'f2a2d7', '84b8b4', '82a2d1', 'a791eb', 'fbc8f5', '7c957a', '84c4a4',
+                          '79d7ea', 'a2aff2', 'a2b884', '82d189', '91ebd4', 'c9e5fa', 'b8a784', 'b9ca89', '91eb91',
+                          'c9fce9', '957686', 'c49484', 'eade7a', 'c3f2a2', 'b884af', 'd1828f', 'ebbd91', 'f7f9c4',
+                          '797699', 'b484c4', 'ea79bb', 'f2a9a2', '8495b8', '9d82d1', 'ea91eb', 'ffc8d4', '76958d',
+                          '84b4c4', '7982ea', 'd1a2f2', '84b88d', '82d1c4', '91beeb', 'd2c6fa', '969976', '94c484',
+                          '79eaa8'),
+            'is_24_bit': False,
+            'is_custom': True
+        }
+    ]
+
+    palettes_types_pending = default_palette_data + custom_palette_data
+
+    for palette in palettes_types_pending:
+        Palette.create(palette_id=palette['palette_id'], is_24_bit=palette['is_24_bit'], is_custom=palette['is_custom'],
+                       name=palette['name'], description=palette['description'], nickname=palette['nickname'],
+                       is_included_with_repo=True, time_created=946706400, color_set=palette['color_set'])
diff --git a/bitglitter/config/loggingset.py b/bitglitter/config/loggingset.py
deleted file mode 100644
index 9d65909..0000000
--- a/bitglitter/config/loggingset.py
+++ /dev/null
@@ -1,46 +0,0 @@
-import datetime
-import logging
-import os
-
-
-def logging_setter(logging_level, logging_print, logging_save_output):
-    '''This is what the logging level and output during the write operation.  It's worth nothing that this is the ONLY
-    function that checks parameters outside of verify_write_parameters, because that function's logging outputs hinge in
-    the configuration for logging.
-    '''
-
-    # First, we're checking the parameters are valid.
-    acceptable_level_words = [False, 'debug', 'info']
-    if logging_level not in acceptable_level_words:
-        raise ValueError(f"{logging_level} is not a valid input for loggingLevel.  Only 'info', 'debug', and False are "
-                         f"allowed.")
-
-    if not isinstance(logging_print, bool):
-        raise ValueError("Only booleans are allowed for loggingPrint.")
-
-    if not isinstance(logging_save_output, bool):
-        raise ValueError("Only booleans are allowed for loggingSaveOutput.")
-
-    logging_level_dict = {False: None, 'debug': logging.DEBUG, 'info': logging.INFO}
-
-    # Next, we're setting each of the loggers if they are enabled.
-    if logging_save_output:
-
-        if not os.path.isdir('logs'):
-            os.mkdir('logs')
-        log_output_name= f"logs\\{datetime.datetime.now().strftime('%Y-%m-%d %H-%M-%S')}.txt"
-        output_log = logging.getLogger()
-        output_log_handler = logging.FileHandler(log_output_name)
-        output_log_formatter = logging.Formatter("%(asctime)s %(levelname)s %(message)s")
-        output_log_handler.setFormatter(output_log_formatter)
-        output_log.addHandler(output_log_handler)
-        output_log.setLevel(logging_level_dict[logging_level])
-
-    if logging_print:
-
-        print_log = logging.getLogger()
-        print_log_handler = logging.StreamHandler()
-        print_log_formatter = logging.Formatter("%(asctime)s.%(msecs)03d %(levelname)s %(message)s", "%H:%M:%S")
-        print_log_handler.setFormatter(print_log_formatter)
-        print_log.addHandler(print_log_handler)
-        print_log.setLevel(logging_level_dict[logging_level])
\ No newline at end of file
diff --git a/bitglitter/config/palettefunctions.py b/bitglitter/config/palettefunctions.py
new file mode 100644
index 0000000..137327f
--- /dev/null
+++ b/bitglitter/config/palettefunctions.py
@@ -0,0 +1,224 @@
+import ast
+import base64
+import logging
+import time
+
+from bitglitter.config.config import session
+from bitglitter.config.palettemodels import Palette
+from bitglitter.utilities.palette import get_palette_id_from_hash, render_sample_frame
+from bitglitter.validation.utilities import proper_string_syntax
+from bitglitter.validation.validatepalette import custom_palette_values_validate
+
+
+def _convert_palette_to_dict(palette):
+    """Minimizing duplicate code"""
+    return {'is_24_bit': palette.is_24_bit, 'is_custom': palette.is_custom, 'is_included_with_repo':
+            palette.is_included_with_repo, 'palette_id': palette.palette_id, 'name': palette.name, 'description':
+            palette.description, 'nickname': palette.nickname, 'color_set': palette.convert_colors_to_tuple(),
+            'color_distance': palette.color_distance, 'number_of_colors': palette.number_of_colors, 'bit_length':
+            palette.bit_length, 'time_created': palette.time_created, 'base64_string': palette.base64_string}
+
+
+def _return_palette(palette_id=None, palette_nickname=None):
+    """Internal function for returning the actual palette model.  The function below is to return an end-user friendly
+    dictionary model.
+    """
+    if not palette_id and not palette_nickname:
+        raise ValueError('Must include palette_id or palette_nickname to return Palette object.')
+    if palette_id:
+        palette = session.query(Palette).filter(Palette.palette_id == palette_id).first()
+    else:
+        palette = session.query(Palette).filter(Palette.nickname == palette_nickname).first()
+    if palette:
+        return palette
+    else:
+        raise ValueError('No existing palettes with this palette_id or palette_nickname.')
+
+
+def return_palette(palette_id=None, palette_nickname=None):
+    palette = _return_palette(palette_id, palette_nickname)
+    if palette:
+        return _convert_palette_to_dict(palette)
+    else:
+        return None
+
+
+def add_custom_palette(palette_name, color_set, nickname=None, palette_description=''):
+    custom_palette_values_validate(palette_name, palette_description, color_set)
+
+    proper_string_syntax(palette_name)
+    if palette_description:
+        proper_string_syntax(palette_description)
+
+    palette = session.query(Palette).filter(Palette.name == palette_name).first()
+    if palette:
+        raise ValueError(f'Name {palette_name} is already taken, please choose another.')
+
+    if nickname:
+        proper_string_syntax(nickname)
+        palette = session.query(Palette).filter(Palette.nickname == nickname).first()
+        if palette:
+            raise ValueError(f"Nickname '{nickname}' is already taken, please choose another.")
+
+    new_palette = Palette.create(is_valid=True, is_custom=True, name=palette_name, palette_id='', nickname=nickname,
+                                 description=palette_description, time_created=int(time.time()), color_set=color_set)
+
+    return new_palette.palette_id
+
+
+def remove_custom_palette(palette_id=None, nickname=None):
+    """Removes custom palette completely from the database."""
+
+    palette = _return_palette(palette_id, nickname)
+    palette.delete()
+
+
+def edit_nickname_to_custom_palette(palette_id=None, existing_nickname=None, new_nickname=None):
+    """This changes the nickname of the given palette to something new, first checking if its valid."""
+
+    if not new_nickname:
+        raise ValueError('Must include a new nickname')
+
+    palette = _return_palette(palette_id, existing_nickname)
+    already_existing_with_nickname = session.query(Palette).filter(Palette.nickname == new_nickname).first()
+    if already_existing_with_nickname:
+        raise ValueError(f'Palette {already_existing_with_nickname.name} already uses this nickname.')
+    else:
+        palette.nickname = new_nickname
+        palette.save()
+
+
+def remove_custom_palette_nickname(palette_id=None, existing_nickname=None):
+    """Removes the palette nickname.  This does not delete the palette, only the nickname."""
+
+    palette = _return_palette(palette_id, existing_nickname)
+    palette.nickname = None
+    palette.save()
+
+
+def remove_all_custom_palette_nicknames():
+    """Removes all custom palette nicknames, including those included with default data.  This does not delete the
+    palettes themselves.
+    """
+
+    palettes = session.query(Palette).filter(Palette.is_custom)
+    for palette in palettes:
+        palette.nickname = None
+        palette.save()
+
+
+def return_all_palettes():
+    palettes = session.query(Palette).all()
+    returned_list = []
+    for palette in palettes:
+        returned_list.append(_convert_palette_to_dict(palette))
+    return returned_list
+
+
+def return_default_palettes():
+    palettes = session.query(Palette).filter(Palette.is_custom == False)
+    returned_list = []
+    for palette in palettes:
+        returned_list.append(_convert_palette_to_dict(palette))
+    return returned_list
+
+
+def return_custom_palettes():
+    palettes = session.query(Palette).filter(Palette.is_custom == True)
+    returned_list = []
+    for palette in palettes:
+        returned_list.append(_convert_palette_to_dict(palette))
+
+    return returned_list
+
+
+def remove_all_custom_palettes():
+    """Removes all custom palettes from the database."""
+    session.query(Palette).filter(Palette.is_custom).delete()
+    session.commit()
+    return True
+
+
+def generate_sample_frame(directory_path, palette_id=None, palette_nickname=None, all_palettes=False,
+                          include_default=False, pixel_width=20, block_height=20, block_width=20):
+    """Prints a small sample frame of a given palette to give an idea of its appearance in normal rendering, selecting
+    random colors from the palette for each of the blocks.  Alternatively, if all_palettes=True, all palettes in the
+    database will be generated.  Argument include_default toggles whether default palettes are included as well.  Can
+    optionally control the size of the sample frames with the last 3 arguments.
+    """
+
+    if not all_palettes:
+        palette = _return_palette(palette_id=palette_id, palette_nickname=palette_nickname)
+        render_sample_frame(palette.name, palette.convert_colors_to_tuple(), palette.is_24_bit, directory_path,
+                            block_width, block_height, pixel_width)
+    else:
+        if include_default:
+            palettes = session.query(Palette).all()
+        else:
+            palettes = session.query(Palette).filter(Palette.is_custom)
+        for palette in palettes:
+            render_sample_frame(palette.name, palette.convert_colors_to_tuple(), palette.is_24_bit, directory_path,
+                                block_width, block_height, pixel_width)
+
+
+def import_palette_base64(base64_string):
+    decoded_string = base64.b64decode(base64_string.encode()).decode()
+    returned_list = decoded_string.split('\\\\')
+    palette_id = returned_list[0]
+    palette_name = returned_list[1]
+    palette_description = returned_list[2]
+    time_created = returned_list[3]
+    color_set_str = returned_list[4]
+    invalid_characters = returned_list[5]
+    if invalid_characters != '':
+        raise ValueError('Corrupted string.  Please ensure you have the full b64 string and try again.')
+
+    # Validating data to ensure no funny business
+    calculated_hash = get_palette_id_from_hash(palette_name, palette_description, time_created, color_set_str)
+    if calculated_hash != palette_id:
+        raise ValueError('Corrupted string.  Please ensure you have the full b64 string and try again.')
+
+    palette = session.query(Palette).filter(Palette.palette_id == palette_id).first()
+    if palette:
+        raise ValueError('Palette already exists locally!')
+    palette = session.query(Palette).filter(Palette.name == palette_name).first()
+    if palette:
+        raise ValueError('Palette with this name already exists.')
+
+    color_set_list = ast.literal_eval(color_set_str)
+    custom_palette_values_validate(palette_name, palette_description, color_set_list)
+
+    palette = Palette.create(palette_id=palette_id, is_valid=True, is_custom=True, name=palette_name,
+                             description=palette_description, time_created=time_created, color_set=color_set_list)
+
+    return palette.palette_id
+
+
+def export_palette_base64(palette_id=None, palette_nickname=None):
+    palette = _return_palette(palette_id=palette_id, palette_nickname=palette_nickname)
+    if not palette.is_valid:
+        raise ValueError('Cannot export invalid palettes')
+
+    return palette.base64_string
+
+
+def import_custom_palette_from_header(palette_id, stream_header_palette_id, palette_name, palette_description,
+                                      time_created, number_of_colors, color_list):
+    """Validates values, and creates and returns palette."""
+    if palette_id != stream_header_palette_id:
+        logging.warning('Corrupted data in palette header, cannot continue.  Aborting...')
+        return False
+
+    calculated_id = get_palette_id_from_hash(palette_name, palette_description, time_created, str(color_list))
+    if calculated_id != stream_header_palette_id:
+        logging.warning('Calculated palette ID / Stream header palette ID mismatch, cannot continue.  Aborting...')
+        return False
+
+    if custom_palette_values_validate(palette_name, palette_description, color_list) == False or \
+            len(color_list) != number_of_colors:
+        logging.warning('Corrupted palette header values, cannot continue.  Aborting...')
+        return False
+
+    palette = Palette.create(palette_id=palette_id, is_valid=True, is_custom=True, name=palette_name,
+                             description=palette_description, time_created=time_created, color_set=color_list)
+    return {'palette': palette}
diff --git a/bitglitter/config/palettemodels.py b/bitglitter/config/palettemodels.py
new file mode 100644
index 0000000..acfc419
--- /dev/null
+++ b/bitglitter/config/palettemodels.py
@@ -0,0 +1,115 @@
+from sqlalchemy import Boolean, Column, Float, Integer, String, UniqueConstraint
+
+import base64
+import math
+import time
+
+from bitglitter.config.config import engine, SQLBaseClass
+from bitglitter.utilities.palette import BitsToColor, ColorsToBits, convert_hex_to_rgb, get_color_distance, \
+    get_palette_id_from_hash
+
+
+class Palette(SQLBaseClass):
+    __tablename__ = 'palettes'
+    __abstract__ = False
+    is_24_bit = Column(Boolean, default=False)
+    is_custom = Column(Boolean, default=True)
+    is_included_with_repo = Column(Boolean, default=False)  # for differentiating other people's colors & our fancy ones
+
+    palette_id = Column(String, unique=True, nullable=False)
+    name = Column(String, unique=True, nullable=False)
+    description = Column(String)
+    nickname = Column(String, nullable=True, unique=True)
+    color_set = Column(String)
+    color_distance = Column(Float, default=0, nullable=False)
+    number_of_colors = Column(Integer, default=0, nullable=False)
+    bit_length = Column(Integer, default=0, nullable=False)
+    time_created = Column(Integer, default=time.time)
+    base64_string = Column(String)
+    is_valid = Column(Boolean)
+
+    @classmethod
+    def create(cls, color_set, **kwargs):
+        object_ = super().create(**kwargs)
+        object_._initialize_colors(color_set)
+        if object_.is_custom:
+            assembled_string = '\\\\'.join(
+                [object_.palette_id, object_.name, object_.description, str(object_.time_created),
+                 str(object_.convert_colors_to_tuple())]) + '\\\\'
+            object_.base64_string = base64.b64encode(assembled_string.encode()).decode()
+        object_.save()
+        return object_
+
+    __table_args__ = (
+        UniqueConstraint('palette_id'),
+    )
+
+    def __str__(self):
+        palette_type = 'Custom' if self.is_custom else 'Default'
+        return f'{palette_type} Palette - {self.name} - {self.number_of_colors} Colors'
+
+    def _calculate_palette_math(self, color_set, save=True):
+        """Runs during model creation and when color set is updated."""
+
+        self.color_distance = get_color_distance(color_set)
+        self.number_of_colors = len(color_set)
+        is_valid = math.log2(self.number_of_colors).is_integer()
+        if is_valid:
+            self.bit_length = int(math.log(self.number_of_colors, 2))
+        else:
+            self.bit_length = 0
+        self.is_valid = is_valid
+
+        if save:  # Added to prevent repetitive saves if used in other methods
+            self.save()
+
+    def convert_colors_to_tuple(self):
+        """Since all of their colors are stored as a single string for speed, this function retrieves it and returns
+        them in a more usable list format.
+        """
+
+        if not self.is_24_bit:
+            string_split = self.color_set.split('|')
+            returned_list = []
+            for piece in string_split:
+                channels = piece.split(',')
+                channels = [int(channel) for channel in channels]
+                returned_list.append((channels[0], channels[1], channels[2]))
+            return returned_list
+        else:
+            return None
+
+    def _initialize_colors(self, color_set):
+        """An internal method that blindly accepts tuples.  Use palettefunctions functions for prior necessary
+        validation of values.
+        """
+
+        color_set_cleaned = convert_hex_to_rgb(color_set) if color_set else None
+        if not self.is_24_bit:
+            self._calculate_palette_math(color_set_cleaned, save=False)
+
+            string_list = []
+            for color in color_set_cleaned:
+                to_string = [str(channel) for channel in color]
+                string_list.append(','.join(to_string))
+
+            self.color_set = '|'.join(string_list)
+        else:
+            self.bit_length = 24
+            self.color_distance = 0
+            self.number_of_colors = 16777216
+
+        if self.is_custom:
+            self.palette_id = get_palette_id_from_hash(self.name, self.description, self.time_created,
+                                                       color_set_cleaned)
+
+    def return_encoder(self):
+        color_set_tupled = self.convert_colors_to_tuple()
+        return BitsToColor(color_set_tupled, self.bit_length, self.name)
+
+    def return_decoder(self):
+        color_set_tupled = self.convert_colors_to_tuple()
+        return ColorsToBits(color_set_tupled, self.bit_length, self.name)
+
+
+SQLBaseClass.metadata.create_all(engine)
diff --git a/bitglitter/config/presetfunctions.py b/bitglitter/config/presetfunctions.py
new file mode 100644
index 0000000..a9f4f1e
--- /dev/null
+++ b/bitglitter/config/presetfunctions.py
@@ -0,0 +1,51 @@
+from bitglitter.config.config import session
+from bitglitter.config.presetmodels import Preset
+from bitglitter.validation.validatewrite import write_preset_validate
+
+
+def _convert_preset_to_dict(preset):
+    return {'nickname': preset.nickname, 'datetime_created': preset.datetime_created, 'output_mode': preset.output_mode,
+            'compression_enabled': preset.compression_enabled, 'scrypt_n': preset.scrypt_n, 'scrypt_r': preset.scrypt_r,
+            'scrypt_p': preset.scrypt_p, 'cpu_cores': preset.cpu_cores, 'stream_palette_id': preset.stream_palette_id,
+            'pixel_width': preset.pixel_width, 'block_height': preset.block_height, 'block_width': preset.block_width,
+            'frames_per_second': preset.frames_per_second}
+
+
+def add_new_preset(nickname, output_mode='video', compression_enabled=True, scrypt_n=14, scrypt_r=8, scrypt_p=1,
+                   max_cpu_cores=0, stream_palette_id='6', pixel_width=24, block_height=45, block_width=80,
+                   frames_per_second=30):
+    write_preset_validate(nickname, output_mode, compression_enabled, scrypt_n, scrypt_r, scrypt_p, max_cpu_cores,
+                          stream_palette_id, pixel_width, block_height, block_width, frames_per_second)
+    if session.query(Preset).filter(Preset.nickname == nickname).first():
+        raise ValueError(f'\'{nickname}\' already exists as a preset nickname.  Please choose a new name or'
+                         f'delete the existing preset.')
+    new_preset = Preset.create(nickname=nickname, output_mode=output_mode, compression_enabled=compression_enabled,
+                               scrypt_n=scrypt_n, scrypt_r=scrypt_r, scrypt_p=scrypt_p, max_cpu_cores=max_cpu_cores,
+                               stream_palette_id=stream_palette_id, pixel_width=pixel_width, block_height=block_height,
+                               block_width=block_width, frames_per_second=frames_per_second)
+    return new_preset
+
+
+def return_all_preset_data():
+    returned_list = []
+    presets = session.query(Preset).all()
+    for preset in presets:
+        returned_list.append(_convert_preset_to_dict(preset))
+    return presets
+
+
+def return_preset(nickname):
+    preset = session.query(Preset).filter(Preset.nickname == nickname).first()
+    if preset:
+        return _convert_preset_to_dict(preset)
+    else:
+        return {}
+
+
+def remove_preset(nickname):
+    preset = session.query(Preset).filter(Preset.nickname == nickname).first()
+    preset.delete()
+
+
+def remove_all_presets():
+    session.query(Preset).delete()
diff --git a/bitglitter/config/presetmodels.py b/bitglitter/config/presetmodels.py
new file mode 100644
index 0000000..e31c102
--- /dev/null
+++ b/bitglitter/config/presetmodels.py
@@ -0,0 +1,28 @@
+from sqlalchemy import Boolean, Column, DateTime, Integer, String
+
+from datetime import datetime
+
+from bitglitter.config.config import engine, SQLBaseClass
+
+
+class Preset(SQLBaseClass):
+    __tablename__ = 'presets'
+    __abstract__ = False
+
+    nickname = Column(String, unique=True, nullable=False)
+    datetime_created = Column(DateTime, default=datetime.now)
+    output_mode = Column(String, nullable=False)
+    compression_enabled = Column(Boolean, nullable=False)
+    scrypt_n = Column(Integer, nullable=False)
+    scrypt_r = Column(Integer, nullable=False)
+    scrypt_p = Column(Integer, nullable=False)
+    cpu_cores = Column(Integer, nullable=False)
+
+    stream_palette_id = Column(String, nullable=False)
+    pixel_width = Column(Integer, nullable=False)
+    block_height = Column(Integer, nullable=False)
+    block_width = Column(Integer, nullable=False)
+    frames_per_second = Column(Integer, nullable=False)
+
+
+SQLBaseClass.metadata.create_all(engine)
diff --git a/bitglitter/config/readfunctions.py b/bitglitter/config/readfunctions.py
new file mode 100644
index 0000000..2d26935
--- /dev/null
+++ b/bitglitter/config/readfunctions.py
@@ -0,0 +1,277 @@
+import json
+import logging
+from pathlib import Path
+
+import cv2
+
+from bitglitter.config.config import session
+from bitglitter.config.configmodels import Constants
+from bitglitter.config.palettemodels import Palette
+from bitglitter.config.readmodels.readmodels import StreamFrame, StreamSHA256Blacklist
+from bitglitter.config.readmodels.streamread import StreamRead
+from bitglitter.read.decode.headerdecode import initializer_header_validate_decode, metadata_header_validate_decode
+from bitglitter.read.scan.scanhandler import ScanHandler
+from bitglitter.read.scan.scanvalidate import frame_lock_on
+from bitglitter.utilities.filemanipulation import refresh_directory, remove_working_folder
+from bitglitter.utilities.loggingset import logging_setter
+
+logging_setter(logging_level='info', logging_stdout_output=True, logging_txt_output=False)
+
+
+def unpackage(stream_sha256):
+    """Attempt to unpackage as much of the file as possible.  Returns None if stream doesn't exist, returns False if
+    there is a decryption error.
+    """
+
+    stream_read = StreamRead.query.filter(StreamRead.stream_sha256 == stream_sha256).first()
+    if not stream_read:
+        return False
+    constants = session.query(Constants).first()
+    working_directory = Path(constants.WORKING_DIR)
+    refresh_directory(working_directory)
+    results = stream_read.attempt_unpackage(working_directory)
+    stream_read.autodelete_attempt()
+    remove_working_folder(working_directory)
+    return results
+
+
+def return_all_read_information(advanced=False):
+    stream_reads = StreamRead.query.all()
+    returned_list = []
+    for stream_read in stream_reads:
+        returned_list.append(stream_read.return_state(advanced))
+    return returned_list
+
+
+def return_single_read_information(stream_sha256, advanced=False):
+    stream_read = StreamRead.query.filter(StreamRead.stream_sha256 == stream_sha256).first()
+    if not stream_read:
+        return False
+    else:
+        return stream_read.return_state(advanced)
+
+
+def update_decrypt_values(stream_sha256, decryption_key=None, scrypt_n=None, scrypt_r=None, scrypt_p=None):
+    stream_read = StreamRead.query.filter(StreamRead.stream_sha256 == stream_sha256).first()
+    if not stream_read:
+        return False
+    if not stream_read.encryption_enabled:
+        return True
+
+    if decryption_key:
+        stream_read.decryption_key = decryption_key
+    if scrypt_n:
+        stream_read.scrypt_n = scrypt_n
+    if scrypt_r:
+        stream_read.scrypt_r = scrypt_r
+    if scrypt_p:
+        stream_read.scrypt_p = scrypt_p
+    stream_read.toggle_eligibility_calculations(True)
+    return True
+
+
+def attempt_metadata_decrypt(stream_sha256):
+    stream_read = StreamRead.query.filter(StreamRead.stream_sha256 == stream_sha256).first()
+    if not stream_read:
+        return False
+    if not stream_read.file_masking_enabled:
+        return {'error': 'File masking not enabled'}
+    if not stream_read.decryption_key:
+        return {'error': 'No decryption key '}
+    if stream_read.manifest_string:
+        return {'error': 'Metadata has already been decrypted'}
+
+    results = metadata_header_validate_decode(stream_read.encrypted_metadata_header_bytes, None,
+                                              stream_read.decryption_key, True, stream_read.file_masking_enabled,
+                                              stream_read.scrypt_n, stream_read.scrypt_r, stream_read.scrypt_p,
+                                              frame_processor=False)
+
+    if 'bg_version' in results:
+        bg_version = results['bg_version']
+        stream_name = results['stream_name']
+        stream_description = results['stream_description']
+        time_created = results['time_created']
+        manifest_string = results['manifest_string']
+        stream_read.metadata_header_load(bg_version, stream_name, stream_description, time_created, manifest_string)
+        return {'metadata': stream_read.metadata_checkpoint_return()}
+    else:
+        return {'error': 'Incorrect decryption value(s)'}
+
+
+def return_stream_manifest(stream_sha256, return_as_json=False):
+    stream_read = StreamRead.query.filter(StreamRead.stream_sha256 == stream_sha256).first()
+    if not stream_read:
+        return False
+    if not stream_read.manifest_string:
+        return {'error': 'Metadata header not decoded yet'}
+    if stream_read.encrypted_metadata_header_bytes:
+        return {'error': 'Metadata not decrypted yet'}
+    if return_as_json:
+        return stream_read.manifest_string
+    else:
+        return json.loads(stream_read.manifest_string)
+
+
+def remove_partial_save(stream_sha256):
+    stream_read = StreamRead.query.filter(StreamRead.stream_sha256 == stream_sha256).first()
+    if not stream_read:
+        return False
+    stream_read.delete()
+    return True
+
+
+def remove_all_partial_save_data():
+    """Removes all data for partial saves, both files and metadata within some internal classes."""
+    session.query(StreamRead).delete()
+    session.commit()
+    return True
+
+
+def update_stream_read(stream_sha256, auto_delete_finished_stream=None, auto_unpackage_stream=None):
+    """Will get larger as more config options are added; general settings about the stream are changed with this."""
+
+    stream_read = StreamRead.query.filter(StreamRead.stream_sha256 == stream_sha256).first()
+    if not stream_read:
+        return False
+    if isinstance(auto_delete_finished_stream, bool):
+        stream_read.auto_delete_finished_stream = auto_delete_finished_stream
+    if isinstance(auto_unpackage_stream, bool):
+        stream_read.auto_unpackage_stream = auto_unpackage_stream
+    stream_read.save()
+    return True
+
+
+def blacklist_stream_sha256(stream_sha256):
+    if not isinstance(stream_sha256, str):
+        raise ValueError('Must be type str')
+    if len(stream_sha256) != 64:
+        raise ValueError('Stream IDs are 64 characters long')
+    hex_characters = '1234567890abcdef'
+    sha256_lowercase = stream_sha256.lower()
+    for character in sha256_lowercase:
+        if character not in hex_characters:
+            raise ValueError('Not a valid stream ID')
+    existing_blacklist = StreamSHA256Blacklist.query \
+        .filter(StreamSHA256Blacklist.stream_sha256 == sha256_lowercase).first()
+    if existing_blacklist:
+        raise ValueError('Blacklisted stream is already in database')
+
+    stream_read = StreamRead.query.filter(StreamRead.stream_sha256 == sha256_lowercase).first()
+    if stream_read:
+        stream_read.delete()
+    StreamSHA256Blacklist.create(stream_sha256=sha256_lowercase)
+    return True
+
+
+def return_all_blacklist_sha256():
+    returned_list = []
+    blacklisted_streams = StreamSHA256Blacklist.query.all()
+    for blacklisted_item in blacklisted_streams:
+        returned_list.append(blacklisted_item.stream_sha256)
+    return returned_list
+
+
+def remove_blacklist_sha256(stream_sha256):
+    blacklist = StreamSHA256Blacklist.query.filter(StreamSHA256Blacklist.stream_sha256 == stream_sha256) \
+        .first()
+    if not blacklist:
+        return False
+    blacklist.delete()
+    return True
+
+
+def remove_all_blacklist_sha256():
+    session.query(StreamSHA256Blacklist).delete()
+    session.commit()
+    return True
+
+
+def return_stream_frame_data(stream_sha256):
+    stream_read = StreamRead.query.filter(StreamRead.stream_sha256 == stream_sha256).first()
+    if not stream_read:
+        return False
+
+    frames = stream_read.frames.order_by(StreamFrame.frame_number.asc()).all()
+    returned_list = []
+    for frame in frames:
+        returned_list.append({'is_complete': frame.is_complete, 'added_to_progress': frame.added_to_progress,
+                              'payload_bits': frame.payload_bits, 'frame_number': frame.frame_number})
+
+    return returned_list
+
+
+def return_stream_file_data(stream_sha256, advanced=False):
+    stream_read = StreamRead.query.filter(StreamRead.stream_sha256 == stream_sha256).first()
+    if not stream_read:
+        return False
+    files = stream_read.files.all()
+    returned_list = []
+    for file in files:
+        returned_list.append(file.return_state(advanced))
+
+    return returned_list
+
+
+def return_stream_progress_data(stream_sha256):
+    stream_read = StreamRead.query.filter(StreamRead.stream_sha256 == stream_sha256).first()
+    if not stream_read:
+        return False
+    progress = stream_read.progress.all()
+    returned_list = []
+    for progress_group in progress:
+        returned_list.append({'bit_start_position': progress_group.bit_start_position, 'bit_end_position':
+                             progress_group.bit_end_position})
+
+    return returned_list
+
+
+def verify_is_bitglitter_file(file_path: str):
+    """Returns True or False depending on if the image or first video frame is detected as valid.  It does this through
+    testing if it can lock onto the frame, and read the initializer."""
+
+    # Disabling log messages in the other functions; we just want a simple True or False returned.
+    logger = logging.getLogger()
+    logger.disabled = True
+
+    constants = session.query(Constants).first()
+    valid_image_formats = constants.return_valid_image_formats()
+    valid_video_formats = constants.return_valid_video_formats()
+    path = Path(file_path)
+    if path.suffix not in valid_image_formats + valid_video_formats:
+        return False
+
+    if path.suffix in valid_image_formats:
+        frame = cv2.imread(file_path)
+    else:
+        active_video = cv2.VideoCapture(file_path)
+        frame = active_video.read()[1]
+
+    frame_pixel_width = frame.shape[1]
+    frame_pixel_height = frame.shape[0]
+
+    initializer_palette_a = Palette.query.filter(Palette.palette_id == '1').first()
+    initializer_palette_b = Palette.query.filter(Palette.palette_id == '11').first()
+    initializer_palette_a_color_set = initializer_palette_a.convert_colors_to_tuple()
+    initializer_palette_b_color_set = initializer_palette_b.convert_colors_to_tuple()
+    initializer_palette_a_dict = initializer_palette_a.return_decoder()
+    initializer_palette_b_dict = initializer_palette_b.return_decoder()
+
+    lock_on_results = frame_lock_on(frame, None, None, frame_pixel_height, frame_pixel_width,
+                                    initializer_palette_a_color_set, initializer_palette_b_color_set,
+                                    initializer_palette_a_dict, initializer_palette_b_dict)
+    if not lock_on_results:
+        return False
+    block_height = lock_on_results['block_height']
+    block_width = lock_on_results['block_width']
+    pixel_width = lock_on_results['pixel_width']
+
+
+    scan_handler = ScanHandler(frame, True, initializer_palette_a, initializer_palette_a_dict,
+                               initializer_palette_a_color_set, block_height, block_width, pixel_width)
+
+    initializer_bits = scan_handler.return_initializer_bits()['bits']
+
+    if not initializer_header_validate_decode(initializer_bits, block_height, block_width):
+        return False
+
+    return True
\ No newline at end of file
diff --git a/bitglitter/config/readmodels/__init__.py b/bitglitter/config/readmodels/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/bitglitter/config/readmodels/readmodels.py b/bitglitter/config/readmodels/readmodels.py
new file mode 100644
index 0000000..16178a8
--- /dev/null
+++ b/bitglitter/config/readmodels/readmodels.py
@@ -0,0 +1,342 @@
+from bitstring import BitStream
+from sqlalchemy import BLOB, Boolean, Column, ForeignKey, Integer, String
+
+import logging
+import math
+import os
+from pathlib import Path
+
+from bitglitter.config.config import session, SQLBaseClass
+from bitglitter.utilities.compression import decompress_file
+from bitglitter.utilities.cryptography import decrypt_file, get_hash_from_file
+from bitglitter.utilities.filemanipulation import refresh_directory
+
+
+class StreamFrame(SQLBaseClass):
+    __tablename__ = 'stream_frames'
+    __abstract__ = False
+
+    # note- sha256 not included as its only verification prior to addition
+    stream_id = Column(Integer, ForeignKey('stream_reads.id', ondelete='CASCADE'))
+    payload_bits = Column(Integer)  # Length of stream payload bits within this frame, tracked to ensure padding removed
+    payload = Column(BLOB)
+    frame_number = Column(Integer)
+    is_complete = Column(Boolean, default=False)
+    added_to_progress = Column(Boolean, default=False)
+
+    def get_bit_index(self, payload_start_frame, payload_first_frame_bits, payload_bits_per_standard_frame,
+                      total_frames, payload_size_in_bytes):
+        """Returns the start and ending index positions of the greater stream payload, used when calculating progress
+        to tell what specific data is inside of a given frame.
+        """
+
+        if self.frame_number == payload_start_frame:
+            return 0, payload_first_frame_bits - 1
+        elif self.frame_number == total_frames:
+            padding = -1 * ((payload_size_in_bytes * 8) - payload_first_frame_bits - self.payload_bits -
+                            (payload_bits_per_standard_frame * (total_frames - payload_start_frame - 1)))
+            bit_start = (payload_size_in_bytes * 8) - (self.payload_bits + 1)
+            return bit_start, bit_start + (self.payload_bits - padding)
+        else:
+            bit_start = (payload_bits_per_standard_frame * (self.frame_number - payload_start_frame - 1)) + \
+                        payload_first_frame_bits
+            return bit_start, bit_start + self.payload_bits - 1
+
+    def progress_calculated(self):
+        """Was used with StreamDataProgress to calculate progress, and doesn't have to be used again."""
+        self.added_to_progress = True
+        self.save()
+
+    def return_partial_frame_payload(self, local_start_position, local_end_position=None):
+        bits = BitStream(self.payload).read(self.payload_bits)
+        bits.pos = local_start_position
+        if local_end_position:
+            return bits.read(local_end_position - local_start_position + 1)
+        else:
+            return bits.read(bits.len - local_start_position)
+
+    def return_full_frame_payload(self):
+        return BitStream(self.payload).read(self.payload_bits)
+
+    def finalize_frame(self, payload_bits=None):
+        if payload_bits:
+            self.payload_bits = payload_bits.len
+            self.payload = payload_bits.tobytes()
+        else:
+            self.added_to_progress = True  # Removing empty frames from upcoming calculation
+        self.is_complete = True
+        self.save()
+
+    def __str__(self):
+        return f'Frame {self.frame_number}/{self.stream.total_frames} for {self.stream}'
+
+
+class StreamFile(SQLBaseClass):
+    __tablename__ = 'stream_files'
+    __abstract__ = False
+
+    stream_id = Column(Integer, ForeignKey('stream_reads.id', ondelete='CASCADE'))
+    sequence = Column(Integer)
+    start_bit_position = Column(Integer)
+    end_bit_position = Column(Integer)
+    is_processed = Column(Boolean, default=False)  # Successfully unpackaged into raw file
+    is_eligible = Column(Boolean, default=False)  # Eligible to be processed
+    save_path = Column(String)
+
+    raw_file_size_bytes = Column(Integer)
+    raw_file_hash = Column(String)
+    processed_file_size_bytes = Column(Integer)
+    processed_file_hash = Column(String)
+
+    def __str__(self):
+        return f'File {self.name} in {self.stream}'
+
+    def _get_frame_indexes(self, payload_start_frame, payload_first_frame_bits, payload_bits_per_standard_frame):
+        """Get start and end frames for a given file, as well as each frame's local index positions"""
+
+        calculated_size = self.processed_file_size_bytes if self.processed_file_size_bytes else self.raw_file_size_bytes
+        calculated_size *= 8
+
+        # Finding first frame position
+        if self.start_bit_position < payload_first_frame_bits:  # Starts on first payload frame
+            first_file_frame = payload_start_frame
+            first_file_frame_start_position = self.start_bit_position
+            total_frame_bits = payload_first_frame_bits
+        else:  # File starts on frame beyond the first frame containing a payload
+            bits_until_frame = self.start_bit_position - payload_first_frame_bits
+            first_file_frame = payload_start_frame + math.ceil(bits_until_frame / payload_bits_per_standard_frame)
+            frame_start_index = payload_first_frame_bits + ((first_file_frame - payload_start_frame - 1)
+                                                            * payload_bits_per_standard_frame)
+            first_file_frame_start_position = self.start_bit_position - frame_start_index
+            total_frame_bits = payload_bits_per_standard_frame
+
+        # Finding last frame position
+        if total_frame_bits - first_file_frame_start_position >= calculated_size:  # File terminates on same frame
+            last_file_frame = first_file_frame
+            last_file_frame_finish_position = first_file_frame_start_position + calculated_size
+        else:  # File terminates on subsequent frames
+            bits_left = calculated_size - (total_frame_bits - first_file_frame_start_position)
+            frame_difference = math.ceil(bits_left / payload_bits_per_standard_frame)
+            last_file_frame = first_file_frame + frame_difference
+            bits_left = bits_left - (payload_bits_per_standard_frame * (frame_difference - 1))
+            last_file_frame_finish_position = bits_left - 1
+
+        return {'first_frame': first_file_frame, 'first_frame_index': first_file_frame_start_position, 'last_frame':
+            last_file_frame, 'last_frame_index': last_file_frame_finish_position}
+
+    def return_state(self, advanced):
+        save_path = Path(self.save_path)
+        basic_state = {'name': save_path.name, 'raw_file_size_bytes': self.raw_file_size_bytes, 'raw_file_hash':
+                       self.raw_file_hash, 'save_path': self.save_path}
+        advanced_state = {'stream_id': self.stream_id, 'sequence': self.sequence, 'start_bit_position':
+                          self.start_bit_position, 'end_bit_position': self.end_bit_position, 'is_processed':
+                          self.is_processed, 'is_eligible': self.is_eligible, 'processed_file_size_bytes':
+                          self.processed_file_size_bytes, 'processed_file_hash': self.processed_file_hash}
+
+        return basic_state | advanced_state if advanced else basic_state
+
+    def _extract_frame_generator(self, first_frame, first_frame_index, last_frame, last_frame_index):
+
+        first_returned_frame = StreamFrame.query.filter(StreamFrame.stream_id == self.stream_id) \
+            .filter(StreamFrame.frame_number == first_frame).first()
+
+        # 1 frame
+        if first_frame == last_frame:
+            yield first_returned_frame.return_partial_frame_payload(first_frame_index, last_frame_index)
+
+        # 2+ frames
+        elif last_frame > first_frame:
+            # First frame
+            yield first_returned_frame.return_partial_frame_payload(first_frame_index)
+
+            # Middle frames
+            mid_frame_minimum = first_frame + 1
+            mid_frame_maximum = last_frame - 1
+            last_consecutive_frame_number = first_returned_frame.frame_number
+            # Getting groups of 100 frames
+            if last_frame - first_frame > 1:
+                for query_group in range(math.ceil((mid_frame_maximum - mid_frame_minimum + 1) / 100)):
+                    query_minimum = mid_frame_minimum + (query_group * 100)
+                    query_maximum = query_minimum + 99 if query_minimum + 99 <= mid_frame_maximum else mid_frame_maximum
+                    frames = StreamFrame.query.filter(StreamFrame.stream_id == self.stream_id) \
+                        .filter(StreamFrame.frame_number >= query_minimum) \
+                        .filter(StreamFrame.frame_number <= query_maximum) \
+                        .order_by(StreamFrame.frame_number.asc())
+
+                    for frame in frames:
+                        assert frame.frame_number - 1 == last_consecutive_frame_number
+                        last_consecutive_frame_number += 1
+
+                        yield frame.return_full_frame_payload()
+
+            # Last frame
+            last_returned_frame = StreamFrame.query.filter(StreamFrame.stream_id == self.stream_id) \
+                .filter(StreamFrame.frame_number == last_frame).first()
+            yield last_returned_frame.return_partial_frame_payload(0, last_frame_index)
+
+    def extract(self, payload_start_frame, payload_first_frame_bits, payload_bits_per_standard_frame,
+                encryption_enabled, compression_enabled, decryption_key, scrypt_n, scrypt_r, scrypt_p,
+                temp_save_directory):
+        raw_path = Path(self.save_path)
+        logging.info(f'Extracting {raw_path.name} ...')
+        returned_results = self.return_state(advanced=False)
+        refresh_directory(temp_save_directory)  # Temp until a better logic system figured out for file names/temp files
+
+        if os.path.exists(self.save_path):
+            calculated_hash = get_hash_from_file(self.save_path)
+            if self.raw_file_hash == calculated_hash:
+                logging.info(f'File already exists at location with correct SHA-256.  Marking as complete...')
+                returned_results['results'] = 'Success- already exists'
+                self.is_processed = True
+                self.save()
+                return returned_results
+            else:
+                logging.info(f'File with this name already exists at this location: {self.save_path}.  '
+                             f'Cannot extract until removed.  Skipping...')
+                returned_results['results'] = 'Failure- file exists in path'
+                return returned_results
+
+        # Which frame(s) the file resides in as well as their local start and finish bit indexes
+        positions = self._get_frame_indexes(payload_start_frame, payload_first_frame_bits,
+                                            payload_bits_per_standard_frame)
+        logging.debug(positions)
+        first_frame = positions['first_frame']
+        first_frame_index = positions['first_frame_index']
+        last_frame = positions['last_frame']
+        last_frame_index = positions['last_frame_index']
+
+        # File assembly
+        os.makedirs(raw_path.parent, exist_ok=True)
+        assemble_path = raw_path if not self.processed_file_size_bytes else temp_save_directory / 'processing.bin'
+        with open(assemble_path, 'wb') as file_writer:
+            buffer = BitStream()
+            for frame_data in self._extract_frame_generator(first_frame, first_frame_index, last_frame,
+                                                            last_frame_index):
+                buffer += frame_data
+                max_allowable_read = (buffer.len // 8) * 8
+                to_write = buffer.read(max_allowable_read).tobytes()
+                file_writer.write(to_write)
+                buffer = buffer.read(buffer.len - max_allowable_read)
+
+        # Post-assembly integrity check if compression and/or encryption enabled on stream
+        if self.processed_file_size_bytes:
+            calculated_hash = get_hash_from_file(assemble_path)
+            if calculated_hash != self.processed_file_hash or \
+                    assemble_path.stat().st_size != self.processed_file_size_bytes:
+                logging.warning('Assembly mismatch, aborting...')
+                returned_results['results'] = 'Internal assembly error'
+                return returned_results
+
+        if encryption_enabled:
+            if compression_enabled:  # Decrypt + decompress
+                decrypt_path = assemble_path.parent / 'decrypted.bin'
+                try:
+                    decrypt_file(assemble_path, decrypt_path, decryption_key, scrypt_n, scrypt_r, scrypt_p)
+                    decompress_file(decrypt_path, self.save_path)
+                    calculated_hash = get_hash_from_file(self.save_path)
+                    if calculated_hash != self.raw_file_hash:
+                        logging.warning('Post-decryption/decompression SHA-256 failure, aborting...')
+                        returned_results['results'] = 'Internal assembly error'
+                        return returned_results
+                except ValueError:
+                    logging.warning('Incorrect decryption values, aborting unpackaging of this stream...')
+                    returned_results['results'] = 'Cannot decrypt'
+                    return returned_results
+
+            else:  # Decryption only
+                try:
+                    decrypt_file(assemble_path, self.save_path, decryption_key, scrypt_n, scrypt_r, scrypt_p)
+                except ValueError:
+                    logging.warning('Incorrect decryption values, aborting unpackaging of this stream...')
+                    returned_results['results'] = 'Cannot decrypt'
+                    return returned_results
+
+        elif compression_enabled:  # Just decompression
+            decompress_file(assemble_path, self.save_path)
+            calculated_hash = get_hash_from_file(self.save_path)
+            if calculated_hash != self.raw_file_hash:
+                logging.warning('Post-decompression SHA-256 failure, aborting...')
+                returned_results['results'] = 'Internal assembly error'
+                os.remove(self.save_path)
+                return returned_results
+
+        else:  # No compression, encryption
+            calculated_hash = get_hash_from_file(self.save_path)
+            if calculated_hash != self.raw_file_hash:
+                logging.warning('Assembly raw file mismatch, aborting...')
+                returned_results['results'] = 'Internal assembly error'
+                os.remove(self.save_path)
+                return returned_results
+
+        self.is_processed = True
+        self.save()
+        returned_results['results'] = 'Success'
+        logging.debug('Extraction successful')
+        return returned_results
+
+
+class StreamDataProgress(SQLBaseClass):
+    """Aside from tracking progress of frames, we also need to account for what index slices of the overall payload we
+    have saved/processed.  This is because files can be extracted from incomplete streams; this is the mechanism to
+    track and calculate that, by seeing if file start:end falls within the current coverage.
+    """
+
+    __tablename__ = 'stream_data_progress'
+    __abstract__ = False
+
+    stream_id = Column(Integer, ForeignKey('stream_reads.id', ondelete='CASCADE'))
+    bit_start_position = Column(Integer)
+    bit_end_position = Column(Integer)
+
+    def __str__(self):
+        return f'Progress slice for {self.stream.stream_name} - bit pos [{self.bit_start_position}:' \
+               f'{self.bit_end_position}]'
+
+    @classmethod
+    def create(cls, stream_id, bit_start_position, bit_end_position, **kwargs):
+        """Before an object is created, checking to see if an adjacent payload frame has already been read.  Rather than
+        making a new object, the old instance will 'pool' the new edges together.
+        """
+
+        identical_progress = session.query(StreamDataProgress).filter(StreamDataProgress.stream_id == stream_id) \
+            .filter(StreamDataProgress.bit_end_position == bit_end_position) \
+            .filter(StreamDataProgress.bit_start_position == bit_start_position).first()
+        if identical_progress:
+            return
+
+        previous_progress = session.query(StreamDataProgress).filter(StreamDataProgress.stream_id == stream_id) \
+            .filter(StreamDataProgress.bit_end_position == bit_start_position - 1).first()
+        if previous_progress:
+            previous_progress.bit_end_position = bit_end_position
+            previous_progress.save()
+
+        next_progress = session.query(StreamDataProgress).filter(StreamDataProgress.stream_id == stream_id) \
+            .filter(StreamDataProgress.bit_start_position == bit_end_position + 1).first()
+        if next_progress:
+            if previous_progress:
+                previous_progress.bit_end_position = next_progress.bit_end_position
+                previous_progress.save()
+                next_progress.delete()
+            else:
+                next_progress.bit_start_position = bit_start_position
+                next_progress.save()
+
+        if previous_progress or next_progress:
+            return
+
+        # Repopulating kwargs to send them off to super() for creation
+        kwargs['stream_id'] = stream_id
+        kwargs['bit_start_position'] = bit_start_position
+        kwargs['bit_end_position'] = bit_end_position
+        super().create(**kwargs)
+
+
+class StreamSHA256Blacklist(SQLBaseClass):
+    """This model holds the SHA-256 of a stream you want to avoid reading and decoding."""
+    __tablename__ = 'stream_sha256_blacklists'
+    __abstract__ = False
+
+    stream_sha256 = Column(String, unique=True)
+
+    def __str__(self):
+        return self.stream_sha256
diff --git a/bitglitter/config/readmodels/streamread.py b/bitglitter/config/readmodels/streamread.py
new file mode 100644
index 0000000..2ec8813
--- /dev/null
+++ b/bitglitter/config/readmodels/streamread.py
@@ -0,0 +1,397 @@
+#  This model has its own module because of its large size
+
+from bitstring import BitStream
+from sqlalchemy import BLOB, Boolean, Column, Float, func, Integer, String
+from sqlalchemy.orm import relationship
+
+import json
+import logging
+import math
+from pathlib import Path
+import time
+
+from bitglitter.config.config import engine, SQLBaseClass, session
+from bitglitter.config.palettemodels import Palette
+from bitglitter.config.readmodels.readmodels import StreamFrame, StreamFile, StreamDataProgress
+from bitglitter.read.decode.manifest import manifest_unpack
+
+
+class StreamRead(SQLBaseClass):
+    """This serves as the central data container and API for interacting with saved read data."""
+
+    __tablename__ = 'stream_reads'
+    __abstract__ = False
+
+    # Core Metadata (App)
+    time_started = Column(Integer, default=time.time)
+    bg_version = Column(String)
+    protocol_version = Column(Integer)
+    stream_sha256 = Column(String, unique=True, nullable=False)
+    stream_is_video = Column(Boolean, nullable=False)
+    stream_palette_id = Column(String)
+    custom_palette_used = Column(Boolean)
+    custom_palette_loaded = Column(Boolean, default=False)
+    total_frames = Column(Integer)
+    compression_enabled = Column(Boolean)
+    encryption_enabled = Column(Boolean)
+    file_masking_enabled = Column(Boolean)
+    stream_name = Column(String)
+    stream_description = Column(String)
+    time_created = Column(Integer)
+    size_in_bytes = Column(Integer)
+    output_directory = Column(String)
+    manifest_string = Column(String)
+
+    # Header Management
+    carry_over_header_bytes = Column(BLOB)
+    carry_over_header_bits = Column(Integer, default=0)
+
+    # Read State
+    payload_start_frame = Column(Integer)
+    payload_first_frame_bits = Column(Integer)
+    payload_bits_per_standard_frame = Column(Integer)
+    palette_header_size_bytes = Column(Integer)
+    palette_header_sha256 = Column(String)
+    metadata_header_size_bytes = Column(Integer)
+    metadata_header_sha256_raw = Column(String)
+    stream_header_complete = Column(Boolean, default=False)
+    metadata_header_complete = Column(Boolean, default=False)
+    palette_header_complete = Column(Boolean, default=False)
+    completed_frames = Column(Integer, default=0)
+    highest_consecutive_setup_frame_read = Column(Integer, default=0)  # Important for initial metadata grab
+    encrypted_metadata_header_bytes = Column(BLOB)  # Used when file masking and incorrect key.  Flushed when correct.
+
+    # Operation State
+    active_this_session = Column(Boolean, default=True)
+    recalculate_eligibility = Column(Boolean, default=True)  # Toggles file eligibility stuff if new data (or not)
+    all_frames_accounted_for = Column(Boolean, default=False)  # all frames are saved in DB
+    total_files = Column(Integer)
+    completed_files = Column(Integer, default=0)
+    is_complete = Column(Boolean, default=False)  # is unpackaged
+
+    # Unpackage State
+    stop_at_metadata_load = Column(Boolean)
+    auto_unpackage_stream = Column(Boolean)
+    auto_delete_finished_stream = Column(Boolean)
+    metadata_checkpoint_ran = Column(Boolean, default=False)
+    highest_processed_frame = Column(Integer)
+    progress_complete = Column(Boolean, default=False)
+
+    # Geometry
+    pixel_width = Column(Float)
+    block_height = Column(Integer)
+    block_width = Column(Integer)
+
+    # Crypto
+    scrypt_n = Column(Integer)
+    scrypt_r = Column(Integer)
+    scrypt_p = Column(Integer)
+    decryption_key = Column(String)
+    metadata_is_decrypted = Column(Boolean, default=False)
+
+    # Relationships
+    frames = relationship('StreamFrame', backref='stream', cascade='all,delete-orphan', lazy='dynamic')
+    files = relationship('StreamFile', backref='stream', cascade='all,delete-orphan', lazy='dynamic')
+    progress = relationship('StreamDataProgress', backref='stream', cascade='all,delete-orphan', lazy='dynamic')
+
+    def __str__(self):
+        if self.stream_name:
+            return f'"{self.stream_name}" | {self.stream_sha256}'
+        else:
+            return self.stream_sha256
+
+    def _refresh_save_directory(self):
+        save_directory = Path(self.output_directory)
+        if not save_directory.exists():
+            logging.debug('Directory for read doesn\'t exist, creating...')
+            save_directory.mkdir()
+
+    def return_state(self, advanced=False):
+        palette_name = None
+        if self.custom_palette_loaded and self.custom_palette_used or not self.custom_palette_used:
+            palette = Palette.query.filter(Palette.palette_id == self.stream_palette_id).first()
+            palette_name = palette.name
+        basic_state = {'time_started': self.time_started, 'bg_version': self.bg_version, 'protocol_version':
+                       self.protocol_version, 'stream_sha256': self.stream_sha256, 'stream_is_video':
+                       self.stream_is_video, 'stream_palette_id': self.stream_palette_id, 'palette_name': palette_name,
+                       'custom_palette_used': self.custom_palette_used, 'total_frames': self.total_frames,
+                       'compression_enabled': self.compression_enabled, 'encryption_enabled': self.encryption_enabled,
+                       'file_masking_enabled': self.file_masking_enabled, 'stream_name': self.stream_name,
+                       'stream_description': self.stream_description, 'time_created': self.time_created,
+                       'size_in_bytes': self.size_in_bytes, 'output_directory': self.output_directory, 'pixel_width':
+                       self.pixel_width, 'block_height': self.block_height, 'block_width': self.block_width,
+                       'scrypt_n': self.scrypt_n, 'scrypt_r': self.scrypt_r, 'scrypt_p': self.scrypt_p,
+                       'metadata_is_decrypted': self.metadata_is_decrypted, 'total_files': self.total_files,
+                       'completed_files': self.completed_files, 'is_complete': self.is_complete,
+                       'stop_at_metadata_load': self.stop_at_metadata_load, 'auto_unpackage_stream':
+                       self.auto_unpackage_stream, 'auto_delete_finished_stream': self.auto_delete_finished_stream,
+                       }
+        advanced_state = {'custom_palette_loaded': self.custom_palette_loaded, 'manifest_string': self.manifest_string,
+                          'carry_over_header_bits': self.carry_over_header_bits, 'payload_start_frame':
+                          self.payload_start_frame, 'payload_first_frame_bits': self.payload_first_frame_bits,
+                          'payload_bits_per_standard_frame': self.payload_bits_per_standard_frame,
+                          'palette_header_size_bytes': self.palette_header_size_bytes, 'palette_header_hash':
+                          self.palette_header_sha256, 'metadata_header_size_bytes': self.metadata_header_size_bytes,
+                          'metadata_header_sha256': self.metadata_header_sha256_raw, 'stream_header_complete':
+                          self.stream_header_complete, 'metadata_header_complete': self.metadata_header_complete,
+                          'palette_header_complete': self.palette_header_complete, 'completed_frames':
+                          self.completed_frames, 'highest_consecutive_setup_frame_read':
+                          self.highest_consecutive_setup_frame_read, 'active_this_session': self.active_this_session,
+                          'recalculate_eligibility': self.recalculate_eligibility, 'all_frames_accounted_for':
+                          self.all_frames_accounted_for, 'metadata_checkpoint_ran': self.metadata_checkpoint_ran,
+                          'highest_processed_frame': self.highest_processed_frame, 'progress_complete':
+                          self.progress_complete}
+        return basic_state | advanced_state if advanced else basic_state
+
+    def is_payload_ready(self):
+        """Returns a boolean if the setup headers have been processed, and there are strictly payload-only frames left
+        in the stream
+        """
+        return self.stream_header_complete and self.metadata_header_complete and self.palette_header_complete
+
+
+
+    def set_pending_header_bits(self, bits=None):
+        if bits:
+            self.carry_over_header_bits = bits.len
+            self.carry_over_header_bytes = bits.tobytes()
+        else:
+            self.carry_over_header_bits = 0
+            self.carry_over_header_bytes = None
+        self.save()
+
+    def return_pending_header_bits(self):
+        return BitStream(self.carry_over_header_bytes).read(self.carry_over_header_bits)
+
+    def session_activity(self, state: bool, save=True):
+        self.active_this_session = state
+        if save:
+            self.save()
+
+    def toggle_eligibility_calculations(self, state: bool, save=True):
+        self.recalculate_eligibility = state
+        if save:
+            self.save()
+
+    def _calculate_payload_bits_per_frame(self, stream_palette_bit_length):
+        FRAME_HEADER_LENGTH_BITS = 352
+        INITIALIZER_LENGTH_BLOCKS = 580
+        total_blocks = ((self.block_width - int(not self.stream_is_video)) *
+                        (self.block_height - int(not self.stream_is_video)))
+
+        self.payload_bits_per_standard_frame = (total_blocks - (INITIALIZER_LENGTH_BLOCKS *
+                                int(not self.stream_is_video))) * stream_palette_bit_length - FRAME_HEADER_LENGTH_BITS
+        self.save()
+
+    def stream_palette_load(self, stream_palette):
+        self.stream_palette_id = stream_palette.palette_id
+        self.custom_palette_used = stream_palette.is_custom
+        self.custom_palette_loaded = True
+        self._calculate_payload_bits_per_frame(stream_palette.bit_length)
+        self.save()
+
+    def stream_header_load(self, size_in_bytes, total_frames, compression_enabled, encryption_enabled,
+                           file_masking_enabled, metadata_header_length, metadata_header_hash,
+                           custom_palette_header_length, custom_palette_header_hash):
+        logging.debug('Stream read- stream header load')
+        self.size_in_bytes = size_in_bytes
+        self.total_frames = total_frames
+        self.compression_enabled = compression_enabled
+        self.encryption_enabled = encryption_enabled
+        self.file_masking_enabled = file_masking_enabled
+        if not encryption_enabled or encryption_enabled and not file_masking_enabled:
+            self.metadata_is_decrypted = True
+        self.metadata_header_size_bytes = metadata_header_length
+        self.metadata_header_sha256_raw = metadata_header_hash
+        self.palette_header_size_bytes = custom_palette_header_length
+        if not custom_palette_header_length:
+            self.palette_header_complete = True
+        self.palette_header_sha256 = custom_palette_header_hash
+        self.stream_header_complete = True
+
+        if not encryption_enabled:  # Purging password in DB if not needed
+            self.decryption_key = None
+        self.save()
+
+    def metadata_header_load(self, bg_version, stream_name, stream_description, time_created, manifest_string):
+        logging.debug('Stream read- metadata header load')
+        self.metadata_is_decrypted = True
+        self.bg_version = bg_version
+        self.stream_name = stream_name
+        self.stream_description = stream_description
+        self.time_created = time_created
+        self.manifest_string = manifest_string
+        self.metadata_header_complete = True
+
+        # Set output directory
+        new_output_directory = Path(self.output_directory)
+        new_output_directory = new_output_directory / stream_name
+        self.output_directory = str(new_output_directory)
+
+        # Manifest process
+        manifest_dict = json.loads(manifest_string)
+        manifest_unpack(manifest_dict, self.id, new_output_directory)
+        self.total_files = self.files.count()
+
+        # Purging encrypted header bytes and decryption key from DB (if applicable)
+        self.encrypted_metadata_header_bytes = None
+
+        self.save()
+
+    def metadata_checkpoint_return(self):
+        """If stop_at_metadata_load is enabled at read() and this hasn't ran previously, this returns a dictionary of
+        all stream header and metadata attributes, as well as stream header data if its a learned palette.
+        """
+
+        self.metadata_checkpoint_ran = True
+        self.save()
+        palette_name = None
+        if self.custom_palette_loaded and self.custom_palette_used or not self.custom_palette_used:
+            palette = Palette.query.filter(Palette.palette_id == self.stream_palette_id).first()
+            palette_name = palette.name
+        manifest_decrypt_success = True if self.manifest_string else False
+        manifest = self.manifest_string if self.manifest_string else None
+        returned_dict = {'stream_name': self.stream_name, 'stream_sha256': self.stream_sha256, 'bg_version':
+                        self.bg_version, 'stream_description': self.stream_description, 'time_created':
+                        self.time_created, 'manifest': manifest, 'size_in_bytes': self.size_in_bytes, 'total_frames':
+                        self.total_frames, 'compression_enabled': self.compression_enabled, 'encryption_enabled':
+                        self.encryption_enabled, 'file_masking_enabled': self.file_masking_enabled, 'protocol_version':
+                        self.protocol_version, 'block_width': self.block_width, 'block_height': self.block_height,
+                        'manifest_decrypt_success': manifest_decrypt_success, 'stream_palette_id':
+                        self.stream_palette_id, 'palette_name': palette_name}
+        return returned_dict
+
+    def accept_encrypted_metadata_bytes(self, encrypted_metadata_bytes):
+        """If file masking is enabled on the stream and incorrect decryption values are used, this adds the encrypted
+        header to the database until it is decrypted.  It is also triggered if no decryption key is provided at all
+        when masking is enabled.  Running this flags the metadata header as complete (fully read)
+        """
+
+        assert self.metadata_header_size_bytes == len(encrypted_metadata_bytes)
+        self.encrypted_metadata_header_bytes = encrypted_metadata_bytes
+        self.metadata_header_complete = True
+        self.save()
+
+    def new_consecutive_frame(self, frame_number):
+        if frame_number == self.highest_consecutive_setup_frame_read + 1:
+            self.highest_consecutive_setup_frame_read += 1
+            self.save()
+
+    def completed_frame_count_update(self):
+        self.completed_frames = self.frames.filter(StreamFrame.is_complete == True).count()
+        logging.debug(f'Completed frames: {self.completed_frames}/{self.total_frames if self.total_files else "?"}')
+        if self.completed_frames == self.total_frames:
+            logging.info(f'All {self.total_frames} frame(s) have been decoded and are accounted for in stream'
+                         f' {self.stream_name}')
+            self.all_frames_accounted_for = True
+        self.save()
+
+    def check_file_eligibility(self):
+        """Assesses and flags what files can be unpackaged through calculating the decoded bit indexes from finalized
+        frames.
+        """
+
+        # Metadata header itself hasn't been read from frames yet
+        if not self.metadata_header_complete and not self.manifest_string:
+            logging.info('Cannot unpackage, metadata header has not been read from frames yet.')
+            return {'failure': 'Metadata not read from frames yet.'}
+
+        # Header has been decoded but not decrypted
+        if not self.metadata_is_decrypted and self.file_masking_enabled:
+            logging.info('Cannot unpackage, correct decryption key hasn\'t been provided.')
+            return {'failure': 'Metadata not decrypted yet'}
+
+        if not self.recalculate_eligibility:
+            return {}
+
+        # Progress calculate
+        self.highest_processed_frame = session.query(func.max(StreamFrame.frame_number)) \
+            .filter(StreamFrame.stream_id == self.id).one()[0]
+
+        # Bypass these calculations
+        if self.all_frames_accounted_for:
+            if not self.progress_complete:
+                self.progress.delete()
+                self.frames.update({StreamFrame.added_to_progress: True})
+                StreamDataProgress.create(self.id, 0, (self.size_in_bytes * 8) - 1)
+                self.progress_complete = True
+
+        # Incomplete stream, calculating progress
+        elif self.highest_processed_frame and self.manifest_string:
+            logging.info('Calculating progress... this might take some time for longer streams with high frame counts.')
+            for index_range in range(math.ceil(self.highest_processed_frame / 100)):
+                frame_group = StreamFrame.query.filter(StreamFrame.frame_number < (index_range + 1) * 100) \
+                    .filter(StreamFrame.frame_number >= index_range * 100).filter(StreamFrame.stream_id == self.id) \
+                    .filter(StreamFrame.added_to_progress == False)
+                for frame in frame_group:
+                    bit_start, bit_end = frame.get_bit_index(self.payload_start_frame, self.payload_first_frame_bits,
+                                                             self.payload_bits_per_standard_frame, self.total_frames,
+                                                             self.size_in_bytes)
+                    StreamDataProgress.create(self.id, bit_start, bit_end)
+                    frame.progress_calculated()
+
+        # What files are eligible to be unpackaged that 'fit' inside of the finished data
+        if self.all_frames_accounted_for:
+            self.files.filter(StreamFile.is_eligible == False) \
+                .update({StreamFile.is_eligible: True})
+        else:
+            for progress_cluster in self.progress:
+                self.files.filter(StreamFile.is_eligible == False)\
+                    .filter(StreamFile.is_processed == False) \
+                    .filter(StreamFile.start_bit_position >= progress_cluster.bit_start_position) \
+                    .filter(StreamFile.end_bit_position <= progress_cluster.bit_end_position) \
+                    .update({StreamFile.is_eligible: True})
+
+        self.toggle_eligibility_calculations(False)
+        return {}
+
+    def attempt_unpackage(self, temp_save_directory):
+        """Attempts to extract files from the partial or complete decoded data.  Returns a dictionary object giving a
+        summary of the results.
+        """
+
+        logging.info(f'Unpackaging stream {str(self)}...')
+        elibility_results = self.check_file_eligibility()
+        if 'failure' in elibility_results:
+            return elibility_results
+
+        self._refresh_save_directory()
+
+        # File extract
+        pending_extraction = self.files.filter(StreamFile.is_eligible == True).filter(StreamFile.is_processed == False)
+
+        returned_list = []
+        extracted_file_count = 0
+        for file in pending_extraction:
+            extract_results = file.extract(self.payload_start_frame, self.payload_first_frame_bits,
+                                           self.payload_bits_per_standard_frame, self.encryption_enabled,
+                                           self.compression_enabled, self.decryption_key, self.scrypt_n, self.scrypt_r,
+                                           self.scrypt_p, temp_save_directory)
+            returned_list.append(extract_results)
+            if extract_results['results'] == 'Success':
+                extracted_file_count += 1
+            elif extract_results['results'] == 'Cannot decrypt':
+                logging.warning('Incorrect decryption values provided for stream.  Please change values and try again.'
+                                '  Aborting...')
+                break
+
+        # Is stream complete?
+        self.completed_files = self.files.filter(StreamFile.is_processed == True).count()
+        if self.completed_files == self.total_files:
+            self.is_complete = True
+        self.save()
+
+        return returned_list, extracted_file_count
+
+    def autodelete_attempt(self):
+        if self.auto_delete_finished_stream and self.is_complete:
+            logging.info(f'All files have been extracted.  Deleting {self}...')
+            self.delete()
+
+    def set_payload_start_data(self, payload_start_frame, payload_first_frame_bits):
+        self.payload_start_frame = payload_start_frame
+        self.payload_first_frame_bits = payload_first_frame_bits
+        self.save()
+
+
+SQLBaseClass.metadata.create_all(engine)
diff --git a/bitglitter/filepackager/fileio.py b/bitglitter/filepackager/fileio.py
deleted file mode 100644
index 9b88b1d..0000000
--- a/bitglitter/filepackager/fileio.py
+++ /dev/null
@@ -1,92 +0,0 @@
-import os
-
-#Written by Tanmay Mishra.  See filepackager module for more information.
-
-def copy_file(source, destination, byte_start=None, byte_end=None):
-
-    try:
-        write_file = open(str(destination), 'wb')
-
-        with open(str(source), 'rb') as open_file:
-            write_file.write(open_file.read()[byte_start:byte_end])
-
-        write_file.close()
-        return True
-
-    except:
-        return False
-
-def file_to_bytes(file_name, byte_start=None, byte_end=None):
-
-    try:
-        with open(file_name, 'rb') as open_file:
-            my_bytes = open_file.read()[byte_start:byte_end]
-        return my_bytes
-
-    except:
-        return b''
-
-
-def bytes_to_file(file_name, my_bytes):
-    try:
-
-        with open(file_name, 'wb') as write_file:
-            write_file.write(my_bytes)
-        return True
-    except:
-        return False
-
-def bytes_or_files_to_bytes(items=[]):
-
-    bytes_return = b''
-
-    for item in items:
-        if type(item) is bytes:
-            bytes_return += item
-        if type(item) is str and does_file_exist(item):
-            bytes_from_file = file_to_bytes(item)
-            bytes_return += bytes_from_file
-
-    return bytes_return
-
-def bytes_or_files_to_file(file_name, items=[]):
-
-    try:
-        my_bytes = bytes_or_files_to_bytes(items)
-        return bytes_to_file(file_name, my_bytes)
-    except:
-        delete_file(file_name)
-        return False
-
-def does_file_exist(file_name):
-
-    return os.path.isfile(file_name)
-
-def get_file_size(file_name):
-    try:
-        return os.path.getsize(str(file_name))
-    except:
-        return None
-
-def delete_file(file_name):
-
-    try:
-        os.remove(str(file_name))
-        return True
-    except:
-        return False
-
-def separate_file_name(file_name =""):
-
-    index_ext = 0
-    len_file = len(file_name)
-
-    for i in range(0,len_file):
-        if file_name[(len_file - i):len_file - i + 1] == '.':
-            index_ext = len_file-i
-            break
-
-    if file_name[index_ext:] == file_name:
-        return [file_name[index_ext:], file_name[0:index_ext]]
-    else:
-        return [file_name[0:index_ext], file_name[index_ext:]]
\ No newline at end of file
diff --git a/bitglitter/filepackager/filepackager.py b/bitglitter/filepackager/filepackager.py
deleted file mode 100644
index e5f2a88..0000000
--- a/bitglitter/filepackager/filepackager.py
+++ /dev/null
@@ -1,436 +0,0 @@
-import logging
-import os
-
-import bitglitter.filepackager.fileio as fileio
-
-# filepackager and fileio modules were both written by Tanmay Mishra. https://github.com/tmishra3/
-
-
-class Folder:
-
-
-    def __init__(self, name = None, file_path = None):
-
-        self.contains = []
-        self.current_path = []
-        self.unique_folder_names = {}
-        self.unique_file_names = {}
-
-        if file_path == None:
-
-            self.name = name
-            self.file_path = file_path
-
-        if file_path != None:
-
-            try:
-                self.name = os.path.basename(file_path)
-                self.file_path = file_path
-                self.index(self.file_path)
-            except:
-                self.name = name
-                self.file_path = None
-
-
-    def copy_attributes(self, source_folder):
-
-        self.current_path = source_folder.current_path
-        self.file_path = source_folder.file_path
-        self.contains = source_folder.contains
-        self.name = source_folder.name
-        self.unique_folder_names = source_folder.unique_folder_names
-        self.unique_file_names = source_folder.unique_file_names
-
-
-    def __repr__(self):
-
-        return '[' + self.name + ']'
-
-
-    def __sub__(self, other = ''):
-
-        if type(other) is str:
-
-            if len(self.current_path) == 0:
-
-                for items in self.contains:
-
-                    if items.name == other:
-
-                        self.contains.remove(items)
-                        break
-
-            if len(self.current_path) > 0:
-
-                self.contains[self.current_path[0]] = self.action_path(other,
-                                                                       self.contains[self.current_path[0]],
-                                                                      self.current_path[1:],
-                                                                       action = 'Subtract')
-
-        return self
-
-
-    def __add__(self, other): #Must take in File or Folder.
-
-        if type(other) is File or type(other) is Folder:
-
-            if len(self.current_path) == 0:
-
-                other = self.return_fixed_name(other)
-                self.contains.append(other)
-
-            if len(self.current_path) > 0:
-
-                self.contains[self.current_path[0]] = self.action_path(other,
-                                                                       self.contains[self.current_path[0]],
-                                                                      self.current_path[1:],
-                                                                       action = 'Add')
-
-        return self
-
-
-    def move_into(self, folder_name):
-
-        this_folder = Folder()
-        this_folder.copy_attributes(self)
-
-        for index in range(0, len(self.current_path)):
-            this_folder = this_folder.contains[self.current_path[index]]
-
-        index = 0
-
-        for items in this_folder.contains:
-
-            if items.name == folder_name and type(items) is Folder:
-
-                self.current_path.append(index)
-                break
-
-            index += 1
-
-
-    def move_up(self):
-        if len(self.current_path) > 0:
-            self.current_path = self.current_path[:-1]
-
-
-    def return_fixed_name(self, child_other):
-
-        try:
-            if type(child_other) is Folder:
-                self.unique_folder_names[child_other.name] = self.unique_folder_names[child_other.name]
-            if type(child_other) is File:
-                self.unique_file_names[child_other.name] = self.unique_file_names[child_other.name]
-        except:
-            if type(child_other) is Folder:
-                self.unique_folder_names[child_other.name] = 1
-            if type(child_other) is File:
-                self.unique_file_names[child_other.name] = 1
-        else:
-            if type(child_other) is Folder:
-                self.unique_folder_names[child_other.name] += 1
-            if type(child_other) is File:
-                self.unique_file_names[child_other.name] += 1
-        finally:
-            if type(child_other) is Folder:
-                name_freq = self.unique_folder_names[child_other.name]
-            if type(child_other) is File:
-                name_freq = self.unique_file_names[child_other.name]
-
-        if name_freq > 1:
-            if type(child_other) is Folder:
-                child_other.name = child_other.name + '(' + str(name_freq) + ')'
-            if type(child_other) is File:
-                name = fileio.separate_file_name(child_other.name)
-                child_other.name = name[0] + '(' + str(name_freq) + ')' + name[1]
-
-        return child_other
-
-
-    def action_path(self, value, folder, current_path=[], action='Add'):
-
-        if len(current_path) == 0:
-
-            if action == 'Add':
-
-                if type(value) is Folder:
-                    value = folder.return_fixed_name(value)
-                    folder.contains.append(value)
-
-                if type(value) is File and os.path.isfile(os.path.join(value.filePath, value.name)):
-                    value = folder.return_fixed_name(value)
-                    folder.contains.append(value)
-
-            if action == 'Subtract' and type(value) is str:
-                for items in folder.contains:
-                    if items.name == value:
-                        folder.contains.remove(items)
-                        break
-
-            return folder
-
-        if len(current_path) > 0:
-
-            folder.contains[current_path[0]] = self.action_path(value,
-                                                                folder.contains[current_path[0]],
-                                                                current_path[1:])
-            return folder
-
-
-    def print_folder(self, print_sub = True):
-
-        this_folder = Folder()
-        this_folder.copy_attributes(self)
-
-        for index in range(0, len(self.current_path)):
-
-            this_folder = this_folder.contains[self.current_path[index]]
-
-        self.print_recursive(this_folder, '', print_sub)
-
-
-    def print_recursive(self, this_folder, space ='', print_sub = True):
-        '''Recursively prints folder and it's children.'''
-
-        print(space + "[" + this_folder.name + "]:")
-
-        for items in this_folder.contains:
-            if type(items) is Folder:
-                if print_sub == True:
-                    items.print_recursive(items, space + '    ', print_sub)
-                else:
-                    print('    ' + "[" + items.name + "]:")
-                    print('    ' + "..." + str(len(items.contains)) + " file(s)/folder(s)..." )
-
-            elif type(items) is File:
-                print('    ' + space + "(" + items.name + ")")
-
-
-    def index(self, my_path):
-
-        default_dir = os.getcwd()
-        os.chdir(my_path)
-        current = os.listdir()
-        self.file_path = my_path
-
-        for something in current:
-
-            if os.path.isdir(os.path.join(my_path, something)):
-
-                logging.debug("Directory Found: " + something)
-                new_folder = Folder(name=str(something))
-                new_folder.index(my_path + '/' + something + '/')
-                self.contains.append(new_folder)
-
-            if os.path.isfile(os.path.join(my_path, something)):
-
-                logging.debug("File Found: " + something)
-                new_file = File(my_path + '/' + something)
-                new_file.file_path = my_path
-                self.contains.append(new_file)
-
-        os.chdir(default_dir)
-
-        return self.contains
-
-
-class File:
-
-    def __init__(self, file_path = None):
-
-        if file_path == None:
-
-            self.name = None
-            self.file_path = file_path
-            self.real_name = None
-
-        if file_path != None:
-
-            try:
-                self.real_name = os.path.basename(file_path)
-                self.name = self.real_name
-                self.file_path = '\\'.join(file_path.split('\\')[0:-1])
-            except:
-                self.name = None
-                self.file_path = None
-                self.real_name = None
-
-    def __repr__(self):
-
-        return '(' + self.name + ')'
-
-
-def package(my_folder, file_name, mask):
-
-    root = Folder(my_folder.name)
-    root += my_folder
-    dig(root, file_name + '\\package.dat', no_payload=False)
-    logging.debug('Package created.')
-    if not mask:
-        dig(root, file_name + '\\file_list.txt', no_payload=True)
-        logging.debug("file_list created.")
-    else:
-        logging.debug('fileMask enabled, skipping...')
-    logging.debug("Internal filepackager process complete.")
-
-
-def dig(my_folder, file_name, no_payload):
-
-    open(file_name, 'a+').close()
-    default_dir = os.getcwd()
-    data_file = b''
-    current = my_folder.contains
-
-    for something in current:
-
-        if type(something) is Folder:
-            logging.debug("Directory Found: " + something.name)
-            logging.debug("Writing Directory: " + something.name)
-
-            with open(file_name, 'ab') as write_file:
-                if no_payload == False:
-                    write_file.write(b"<" + something.name.encode() + b">")
-
-            dig(something, file_name, no_payload)
-
-            with open(file_name, 'ab') as write_file:
-                if no_payload == False:
-                    write_file.write(b"</" + something.name.encode() + b">")
-
-            logging.debug("Closing Directory: " + something.name)
-
-        if type(something) is File:
-
-            if something.file_path != None:
-
-                logging.debug("File Found: " + something.name)
-
-                try:
-
-                    if no_payload == False:
-                        data_file += b":" + something.name.encode() + b"|"
-                        data_file += b"{" + str(os.path.getsize(os.path.join(something.file_path,
-                                                                             something.real_name))).encode() + b"}"
-                    else:
-                        data_file += b"|" + something.name.encode() + b"|"
-                        data_file += str(os.path.getsize(os.path.join(something.file_path, something.real_name))) \
-                            .encode()
-                    if no_payload == False:
-                        data_file += open(something.file_path + '/' + something.real_name, 'rb').read()
-                except:
-                    data_file = b":|{0}"
-                    logging.debug("\nERROR: Unable to package file, " + something.name + ", skipping.\n")
-
-
-            if something.file_path == None:
-
-                logging.debug("Virtual File Found: " + something.name)
-
-                data_file += b":" + something.name.encode() + b"|"
-                data_file += b"{0}"
-                data_file += b''
-
-            with open(file_name, 'ab') as open_file:
-
-                logging.debug("Writing File: " + something.name)
-                open_file.write(data_file)
-
-            data_file = b''
-
-    os.chdir(default_dir)
-
-
-def unpackage(file_name, save_path, stream_SHA):
-
-    output_path = save_path
-    if save_path == None:
-        output_path = os.getcwd()
-    original_working_directory = os.getcwd()
-    root_folder_created = False
-
-    with open(file_name, 'rb') as open_file:
-
-        read_byte = b''
-        temp = b''
-        current_file = b''
-
-        while True:
-
-            read_byte = open_file.read(1)
-
-            if read_byte == b'':
-                break
-
-            if read_byte == b'<': #working with folder.
-
-                while True:
-
-                    read_byte = open_file.read(1)
-
-                    if read_byte != b'>':
-
-                        temp += read_byte
-                    else:
-
-                        if temp[0:1] != b'/':
-                            logging.debug("Folder " + temp.decode('utf-8') + ' detected.')
-
-                            if root_folder_created == True:
-                                os.mkdir(os.path.join(output_path,temp.decode('utf-8')))
-                                output_path += '/' + temp.decode('utf-8')
-                                logging.debug("Making Folder: " + temp.decode('utf-8') + " and moving into it.")
-
-                            else:
-                                try:
-                                    os.mkdir(os.path.join(output_path, stream_SHA))
-                                except:
-                                    pass
-                                output_path += '/' + stream_SHA
-                                logging.debug("Making Folder: " + stream_SHA + " and moving into it.")
-                                root_folder_created = True
-
-                            os.chdir(output_path)
-                            temp = b''
-                            break
-
-                        if temp[0:1] == b'/':
-                            logging.debug("Folder termination detected. Moving one directory up.")
-                            os.chdir('..')
-                            output_path = os.getcwd()
-                            temp = b''
-                            break
-
-            if read_byte == b':':
-
-                while True:
-
-                    read_byte = open_file.read(1)
-
-                    if read_byte != b'|':
-                        temp += read_byte
-                    else:
-                        if not os.path.isdir(os.path.join(output_path, temp.decode('utf-8'))):
-                            logging.debug("File " + temp.decode('utf-8') + ' detected.')
-                            open(os.path.join(output_path, temp.decode('utf-8')), 'wb').close()
-                            current_file = temp.decode('utf-8')
-                            temp = b''
-                        break
-            if read_byte == b'{':
-
-                while True:
-
-                    read_byte = open_file.read(1)
-
-                    if read_byte != b'}':
-
-                        temp += read_byte
-                    else:
-
-                        logging.debug("Creating file " + current_file + "...")
-                        with open(os.path.join(output_path, current_file), 'wb') as write_file:
-                            write_file.write(open_file.read(int(temp)))
-                        logging.debug("Data printed to " + current_file)
-                        temp = b''
-                        break
-
-    os.chdir(original_working_directory)
\ No newline at end of file
diff --git a/bitglitter/palettes/palettefunctions.py b/bitglitter/palettes/palettefunctions.py
deleted file mode 100644
index 12f26bf..0000000
--- a/bitglitter/palettes/palettefunctions.py
+++ /dev/null
@@ -1,161 +0,0 @@
-import os
-import time
-
-from bitglitter.config.config import config
-from bitglitter.palettes.paletteutilities import _add_custom_palette_direct, color_distance, return_palette_id
-from bitglitter.utilities.generalverifyfunctions import proper_string_syntax
-
-# All of these functions are for end users except for _dict_popper.
-
-def _dict_popper(id_or_nick):
-    '''This is an internal function used for remove_custom_palette(), edit_nickname_to_custom_palette(), and
-    clear_custom_palette_nicknames().  Since a user can either either the palette ID OR it's nickname as an argument,
-    first we must check whether that key exists.  If it does, it will check both dictionaries in the ColorHandler object
-    for custom colors, custom_palette_list and custom_palette_nickname_list.  It will then delete both dictionary keys
-    (if available), and  then return the object to optionally be modified (or otherwise discarded)
-    '''
-
-    if id_or_nick in config.color_handler.custom_palette_nickname_list:
-        temp_holder = config.color_handler.custom_palette_nickname_list[id_or_nick]
-        del config.color_handler.custom_palette_list[temp_holder.id]
-        return config.color_handler.custom_palette_nickname_list.pop(id_or_nick)
-
-    elif id_or_nick in config.color_handler.custom_palette_list:
-        temp_holder = config.color_handler.custom_palette_list[id_or_nick]
-        if temp_holder.nickname in config.color_handler.custom_palette_nickname_list:
-            del config.color_handler.custom_palette_nickname_list[temp_holder.nickname]
-        return config.color_handler.custom_palette_list.pop(id_or_nick)
-
-    else:
-        raise ValueError(f"'{id_or_nick}' does not exist.")
-
-
-def add_custom_palette(palette_name, palette_description, color_set, optional_nickname =""):
-    '''This function allows you to save a custom palette to be used for future writes.  Arguments needed are name,
-    description, color set, which is a tuple of tuples, and optionally a nickname.  All other object are attributes are
-    added in this process.  Before anything gets added, we need to ensure the arguments are valid.
-    '''
-
-    date_created = str(round(time.time()))
-    name_string = str(palette_name)
-    description_string = str(palette_description)
-    nickname_string = str(optional_nickname)
-
-    # Is name, description legal characters?
-    proper_string_syntax(name_string)
-    proper_string_syntax(description_string)
-
-    # Is nickname legal characters?  Is the name available?
-    proper_string_syntax(nickname_string)
-    if optional_nickname in config.color_handler.custom_palette_nickname_list or optional_nickname in \
-            config.color_handler.custom_palette_list or optional_nickname in config.color_handler.default_palette_list:
-        raise ValueError(f"'{optional_nickname}' is already taken, please choose another nickname.")
-
-    # Verifying color set parameters.  2^n length, 3 values per color, values are type int, values are 0-255.  Finally,
-    # verify colors aren't overlapping (ie, the same color is used twice).
-    if len(color_set) % 2 != 0 or len(color_set) < 2:
-        raise ValueError("Length of color set must be 2^n length (2 colors, 4, 8, etc) with a minimum of two colors.")
-
-    for color_tuple in color_set:
-
-        if len(color_tuple) != 3:
-            raise ValueError("Each color needs 3 entries, for red green and blue.")
-
-        for color in color_tuple:
-            if not isinstance(color, int) or color < 0 or color > 255:
-                raise ValueError("For each RGB value, it must be an integer between 0 and 255.")
-
-    min_distance = color_distance(color_set)
-    if min_distance == 0:
-        raise ValueError("Calculated color distance is 0.  This occurs when you have two identical colors in your"
-              "\npalette.  This breaks the communication protocol.  See BitGlitter guide for more information.")
-
-    '''At this point, assuming no errors were raised, we're ready to instantiate the custom color object.  This function
-    creates an identification for the object.  For as long as the palette exists, this is a permanent ID that can be
-    used as an argument for colorID in write().  This value will get returned at the end of this function.
-    '''
-
-    id = return_palette_id(name_string, description_string, date_created, color_set)
-    _add_custom_palette_direct(name_string, description_string, color_set, min_distance, date_created, id,
-                               nickname_string)
-    return id
-
-
-def remove_custom_palette(id_or_nick):
-    '''Removes custom palette completely from the config file.'''
-
-    _dict_popper(id_or_nick)
-    config.save_session()
-
-
-def edit_nickname_to_custom_palette(id_or_nick, new_name):
-    '''This changes the nickname of the given palette to something new, first checking if it's valid.'''
-
-    if new_name not in config.color_handler.custom_palette_list \
-            and new_name not in config.color_handler.custom_palette_nickname_list \
-            and new_name not in config.color_handler.default_palette_list:
-
-        temp_holder = _dict_popper(id_or_nick)
-        temp_holder.nickname = new_name
-        config.color_handler.custom_palette_list[temp_holder.id] = temp_holder
-        config.color_handler.custom_palette_nickname_list[temp_holder.nickname] = temp_holder
-        config.save_session()
-
-    else:
-        raise ValueError(f"'{new_name}' is already being used, please try another.")
-
-
-def remove_custom_palette_nickname(id_or_nick):
-    '''Removes the palette nickname from the corresponding dictionary.  This does not delete the palette, only the
-    nickname.
-    '''
-
-    temp_holder = _dict_popper(id_or_nick)
-    temp_holder.nickname = ""
-    config.color_handler.custom_palette_list[temp_holder.id] = temp_holder
-    config.save_session()
-
-
-def clear_custom_palette_nicknames():
-    '''Clears all custom palette nicknames.  This does not delete the palettes themselves.'''
-
-    config.color_handler.custom_palette_nickname_list = {}
-
-    for palette in config.color_handler.custom_palette_list:
-
-        temp_holder = config.color_handler.custom_palette_list.pop(palette)
-        temp_holder.nickname = ""
-        config.color_handler.custom_palette_list[temp_holder.id] = temp_holder
-
-    config.save_session()
-
-
-def print_full_palette_list(path):
-    '''Writes a text file to a file path outlining available color palettes.'''
-
-    active_path = os.path.join(os.getcwd(), path)
-
-    with open(active_path + '\\BitGlitter Palette List.txt', 'w') as writer:
-
-        writer.write('*' * 21 + '\nDefault Palettes\n' + '*' * 21 + '\n')
-
-        for some_key in config.color_handler.default_palette_list:
-            writer.write('\n' + str(config.color_handler.default_palette_list[some_key]) + '\n')
-
-        writer.write('*' * 21 + '\nCustom Palettes\n' + '*' * 21 + '\n')
-
-        if config.color_handler.custom_palette_list:
-
-            for some_key in config.color_handler.custom_palette_list:
-                writer.write('\n' + str(config.color_handler.custom_palette_list[some_key]) + '\n')
-
-        else:
-            writer.write('\nNo custom palettes (yet)')
-
-
-def clear_all_custom_palettes():
-    '''Removes all custom palettes from both the ID dictionary and nickname dictionary.'''
-
-    config.colorHandler.custom_palette_nickname_list = {}
-    config.colorHandler.custom_palette_list = {}
-    config.save_session()
\ No newline at end of file
diff --git a/bitglitter/palettes/paletteobjects.py b/bitglitter/palettes/paletteobjects.py
deleted file mode 100644
index d9a6e6e..0000000
--- a/bitglitter/palettes/paletteobjects.py
+++ /dev/null
@@ -1,91 +0,0 @@
-import datetime
-import math
-
-
-class AbstractPalette:
-    '''Setting a foundation for DefaultPalette and CustomPalette objects by defining some functionality.
-    AbstractPalette is not meant to be instantiated, only it's children!
-    '''
-
-    def __init__(self, name, description, color_set, color_distance):
-
-        self.name = name
-        self.description = description
-        self.color_set = color_set
-        self.color_distance = color_distance
-
-        self.id = None
-        self.number_of_colors = len(self.color_set)
-        self.bit_length = int(math.log(self.number_of_colors, 2))
-
-    def __getitem__(self, item):
-
-        return self.color_set[item]
-
-
-class DefaultPalette(AbstractPalette):
-    '''These are palettes that come default with BitGlitter.  They cannot be changed or removed.'''
-
-    def __init__(self, name, description, color_set, color_distance, id):
-
-        super().__init__(name, description, color_set, color_distance)
-        self.id = str(id)
-        self.palette_type = 'default'
-
-
-    def __str__(self):
-
-        return (f'Name: {self.name}\nIdentification Code: {str(self.id)}\nDescription: {self.description}\nBit Length: '
-        f'{str(self.bit_length)}\nNumber of Colors: {str(self.number_of_colors)}\nColor Set: {str(self.color_set)}'
-        f'\nColor Distance: {str(self.color_distance)}\n')
-
-
-class CustomPalette(AbstractPalette):
-    '''This is what custom color palettes become.  It blindly accepts all values; all necessary checks are performed in
-    add_custom_palette() in bitglitter.palettes.palettefunctions.
-    '''
-
-    def __init__(self, name, description, color_set, color_distance, dateCreated, id, nickname):
-
-        super().__init__(name, description, color_set, color_distance)
-
-        self.date_created = int(dateCreated)
-        self.id = id
-        self.nickname = nickname
-        self.palette_type = 'custom'
-
-
-    def __str__(self):
-
-        return (f"Name: {self.name}\nIdentification Code: {str(self.id)}\nNickname: {self.nickname}\nDescription: "
-        f"{self.description}"
-        f"\nDate Created: {datetime.datetime.fromtimestamp(self.date_created).strftime('%A, %B %d, %Y - %I:%M:%S %p')}"
-        f"\nBit Length: {str(self.bit_length)}"
-        f"\nNumber of Colors: {str(self.number_of_colors)}\nColor Set: {str(self.color_set)}"
-        f"\nColor Distance: {str(self.color_distance)}\n")
-
-
-class TwentyFourBitPalette:
-    '''This class doesn't function exactly like the other two classes.  You'll notice it takes no arguments and there
-    are no colors in colorSet.  There is only one object that can exist of this class, because of the specific values
-    needed.  This object is what represents the 24 bit color set.
-    '''
-
-    def __init__(self):
-
-        self.name = "24 bit default"
-        self.description = "EXPERIMENTAL!  ~16.7 million colors.  This will only work in lossless" \
-         " environments, any sort of compression will corrupt the data."
-        self.color_set = None
-        self.number_of_colors = 16777216
-        self.bit_length = 24
-        self.id = str(self.bit_length)
-        self.color_distance = 0
-        self.palette_type = 'default'
-
-
-    def __str__(self):
-
-        return (f'Name: {self.name}\nIdentification Code: {str(self.id)}\nDescription: {self.description}\nBit Length: '
-                f'{str(self.bit_length)}\nNumber of Colors: {str(self.number_of_colors)}\nColor Set: Too many '
-                f'to list (see directly above)\nColor Distance: {str(self.color_distance)}\n')
\ No newline at end of file
diff --git a/bitglitter/palettes/paletteutilities.py b/bitglitter/palettes/paletteutilities.py
deleted file mode 100644
index f34b88f..0000000
--- a/bitglitter/palettes/paletteutilities.py
+++ /dev/null
@@ -1,172 +0,0 @@
-import hashlib
-import itertools
-import logging
-import math
-
-from bitstring import BitArray, ConstBitStream
-
-from bitglitter.config.config import config
-from bitglitter.palettes.paletteobjects import CustomPalette
-
-
-class ValuesToColor:
-    '''This generates a dictionary linking a string binary value to an RGB value.  This is how binary data gets directly
-    converted to colors.  This step required more than a dictionary, as additional logic was required to switch between
-    a standard dictionary used by default and custom palettes, and 24 bit palettes.  They both convert data to colors in
-    different ways, and this provides a single clean interface for that.
-    '''
-
-    def __init__(self, palette, type):
-
-        logging.debug(f'Generating binary : color dictionary for {type}...')
-        self.palette = palette
-        self.bit_length = self.palette.bit_length
-        self.type = type
-        self.return_value = self.generate_dictionary()
-
-    def generate_dictionary(self):
-
-        def twenty_four_bit_values(value):
-
-            red_channel = value.read('uint : 8')
-            green_channel = value.read('uint : 8')
-            blue_channel = value.read('uint : 8')
-            return (red_channel, green_channel, blue_channel)
-
-        color_dict = {}
-        if self.palette.bit_length != 24:
-
-            for value in range(len(self.palette.color_set)):
-
-                temp_bin_holder = str(BitArray(uint=value, length=self.palette.bit_length))
-                temp_bin_holder = ConstBitStream(temp_bin_holder)
-                color_dict[temp_bin_holder] = self.palette.color_set[value]
-
-            return color_dict
-
-        else:
-
-            return twenty_four_bit_values
-
-    def get_color(self, value):
-
-        if self.bit_length != 24:
-            return self.return_value[value]
-
-        else:
-            return self.return_value(value)
-
-
-class ColorsToValue:
-    '''This class does the exact opposite as ValuesToColor.  This first generates a dictionary linking colors to
-    specific bit values, and then get_value() accomplishes that.  It is worth noting that 24 bit color functions
-    differently than the other color palettes, in that it doesn't use a dictionary, but rather converts each byte into
-    an unsigned integer for each of it's three color channels, and then returns that color.
-    '''
-
-    def __init__(self, palette):
-
-        self.palette = palette
-        self.return_value = self.generate_dictionary()
-
-    def generate_dictionary(self):
-
-
-        def twenty_four_bit_values(color):
-
-            outgoing_data = BitArray()
-            for color_channel in color:
-                outgoing_data.append(BitArray(uint=color_channel, length=8))
-
-            return outgoing_data
-
-        value_dict = {}
-
-        if self.palette.bit_length != 24:
-            for value in range(len(self.palette.color_set)):
-                temp_bin_holder = str(BitArray(uint=value, length=self.palette.bit_length))
-                temp_bin_holder = ConstBitStream(temp_bin_holder)
-                value_dict[self.palette[value]] = temp_bin_holder
-            return value_dict
-        else:
-            return twenty_four_bit_values
-
-
-    def get_value(self, color):
-        if self.palette.bit_length != 24:
-            return self.return_value[color]
-        else:
-            return self.return_value(color)
-
-
-def palette_grabber(id_or_nick):
-    '''Goes through each of the dictionaries to return the color object.'''
-
-    if id_or_nick in config.color_handler.default_palette_list:
-        return config.color_handler.default_palette_list[id_or_nick]
-    elif id_or_nick in config.color_handler.custom_palette_list:
-        return config.color_handler.custom_palette_list[id_or_nick]
-    elif id_or_nick in config.color_handler.custom_palette_nickname_list:
-        return config.color_handler.custom_palette_nickname_list[id_or_nick]
-    else:
-        raise ValueError('palette_grabber(): This value is not present.')
-
-
-def _validate_and_add_palette(palette_name, palette_description, date_created, color_set):
-    '''This is solely to input custom palettes without all of the other prompts.  Returns True if validated and added,
-    and false if it isn't.
-    '''
-
-    distance = color_distance(color_set)
-
-    if distance == 0:
-        return False
-
-    if len(color_set) % 2 != 0 or len(color_set) < 2:
-        return False
-
-    id = return_palette_id(palette_name, palette_description, date_created, color_set)
-    _add_custom_palette_direct(palette_name, palette_description, color_set, distance, date_created, id)
-
-    return True
-
-
-def color_distance(palette):
-    '''This function takes in the set of tuples in a palette, and calculates their proximity to each other in RGB space.
-    Higher number denote 'safer' palettes to use in the field, as they are less prone to errors in the field.  Getting 0
-    returned means you have at least a single pair of identical RGB values.  All values must be unique!
-    '''
-
-    local_distances = []
-
-    for unique_set in itertools.combinations(palette, 2):
-
-        active_distance = math.sqrt(
-            ((unique_set[1][0] - unique_set[0][0]) ** 2) + ((unique_set[1][1] - unique_set[0][1])
-            ** 2) + ((unique_set[1][2] - unique_set[0][2]) ** 2))
-        local_distances.append(active_distance)
-
-    return round(min(local_distances), 2)
-
-
-def return_palette_id(name, description, date_created, color_set):
-    '''Taking in the various parameters, this creates a unique ID for the object.'''
-
-    color_set_string = str(color_set)
-    hasher = hashlib.sha256(str(name + description + date_created + color_set_string).encode())
-    return(hasher.hexdigest())
-
-
-def _add_custom_palette_direct(name, description, color_set, distance, date_created, id, nickname=None):
-    '''After validation is done, this function is ran to actually instantiate the palette object, as well as load it
-    into the appropriate dictionaries and save the configuration file.  This should never be ran by itself because it
-    blindly accepts all values!
-    '''
-
-    new_palette = CustomPalette(name, description, color_set, distance, date_created, id, nickname)
-    config.color_handler.custom_palette_list[id] = new_palette
-
-    if nickname:
-        config.color_handler.custom_palette_nickname_list[nickname] = new_palette
-
-    config.save_session()
diff --git a/bitglitter/protocols/protocol_one/read/protocol_one_postprocess.py b/bitglitter/protocols/protocol_one/read/protocol_one_postprocess.py
deleted file mode 100644
index 179ae7f..0000000
--- a/bitglitter/protocols/protocol_one/read/protocol_one_postprocess.py
+++ /dev/null
@@ -1,24 +0,0 @@
-import logging
-
-from bitglitter.protocols.protocol_one.read.protocol_one_postprocessobjects import Decryptor, Decompressor, Unpackager
-
-
-class PostProcessor:
-    def __init__(self, output_path, stream_SHA, working_folder, encryption_enabled, encryption_key, scrypt_n, scrypt_r,
-                 scrypt_p, compression_enabled):
-        '''Upon successfully assembly of the stream, PostProcessor performs the final steps required to output the
-        files- decrypting and decompressing the stream if they were enabled, and then finally unpackaging the files.
-        '''
-
-        self.fully_assembled = False
-
-        decryptor = Decryptor(working_folder, encryption_enabled, encryption_key, scrypt_n, scrypt_r,
-                              scrypt_p, stream_SHA)
-        self.is_decrypted = decryptor
-
-        if decryptor.is_satisfied == True:
-
-            decompressor = Decompressor(working_folder, decryptor.pass_through, compression_enabled)
-            unpackager = Unpackager(decompressor.pass_through, output_path, stream_SHA)
-            self.fully_assembled = True
-            logging.info('Postprocess complete.')
diff --git a/bitglitter/protocols/protocol_one/read/protocol_one_postprocessobjects.py b/bitglitter/protocols/protocol_one/read/protocol_one_postprocessobjects.py
deleted file mode 100644
index 69c3acd..0000000
--- a/bitglitter/protocols/protocol_one/read/protocol_one_postprocessobjects.py
+++ /dev/null
@@ -1,82 +0,0 @@
-import logging
-import os
-
-from bitglitter.filepackager.filepackager import unpackage
-from bitglitter.utilities.filemanipulation import decrypt_file, return_hash_from_file, decompress_file
-
-
-class Decryptor:
-    '''This is the first step of the post-processing once assembly is complete.  This will take the assembled binary,
-    and, if encryption was enabled on this stream, will attempt to decrypt it with the AES key provided.
-    '''
-
-    def __init__(self, working_folder, encryption_enabled, encryption_key, scrypt_n, scrypt_r, scrypt_p, stream_sha):
-
-        input_file = working_folder + '\\assembled.bin'
-        self.pass_through = input_file
-        self.is_satisfied = False
-
-        if encryption_enabled:
-
-            if encryption_key:
-
-                logging.info('Attempting to decrypt with provided key...')
-                logging.debug(f'Trying with key {encryption_key}')
-                self.pass_through = working_folder + '\\decrypted.bin'
-                decrypt_file(input_file, self.pass_through, encryption_key, scrypt_n, scrypt_r, scrypt_p,
-                             remove_input=False)
-
-                # Checking if password is valid
-                if stream_sha == return_hash_from_file(self.pass_through):
-
-                    logging.info('Successfully decrypted.')
-                    self.is_satisfied = True
-
-                else:
-
-                    logging.info("Password incorrect, cannot continue.")
-                    os.remove(self.pass_through)
-
-            else:
-
-                logging.warning(f'Decryption key missing from input argument, cannot continue.')
-
-        else:
-
-            logging.info('Encryption was not enabled on this stream.  Skipping step...')
-            self.is_satisfied = True
-
-
-class Decompressor:
-    '''If compression was enabled on this stream, this object will decompress it.'''
-
-    def __init__(self, working_folder, pass_through, compression_enabled):
-
-        self.pass_through = pass_through
-
-        if compression_enabled:
-            new_path = working_folder + "\\decompressed.dat"
-            logging.info('Decompressing file...')
-            decompress_file(self.pass_through, new_path)
-            self.pass_through = new_path
-            logging.info('Successfully decompressed.')
-
-        else:
-            logging.info('Compression was not enabled on this stream.  Skipping step...')
-
-
-class Unpackager:
-    '''This object takes the decompressed binary and 'unpackages' it into the files and/or folders embedded in it.'''
-
-    def __init__(self, pass_through, output_path, stream_sha):
-
-        if output_path == None:
-            printed_save_location = 'program run folder'
-
-        else:
-            printed_save_location = output_path
-
-        logging.info(f'Unpackaging file(s) at {printed_save_location}...')
-
-        unpackage(pass_through, output_path, stream_sha)
-        logging.info('File(s) successfully unpackaged.')
diff --git a/bitglitter/protocols/protocol_one/write/protocol_one_preprocess.py b/bitglitter/protocols/protocol_one/write/protocol_one_preprocess.py
deleted file mode 100644
index cf137d1..0000000
--- a/bitglitter/protocols/protocol_one/write/protocol_one_preprocess.py
+++ /dev/null
@@ -1,39 +0,0 @@
-import logging
-import time
-
-from bitglitter.protocols.protocol_one.write.protocol_one_preprocessobjects import Packager, Compressor, Encryptor
-from bitglitter.utilities.filemanipulation import refresh_working_folder, return_file_size, return_hash_from_file
-
-
-class PreProcessor:
-    '''The PreProcessor object performs all necessary preparations for the file(s) to be written, prior to rendering.
-    After packaging, and optional compression and encryption, it contains some attributes that will be encoded in
-    headers during the render process.
-    '''
-
-    def __init__(self, active_path, file_list, crypto_key, mask_files, compression_enabled, scrypt_n, scrypt_r,
-                 scrypt_p):
-
-        logging.info("Preprocess initializing...")
-        self.date_created = round(time.time())
-
-        self.active_folder = refresh_working_folder(active_path)
-        packager = Packager(self.active_folder, file_list, mask_files)
-        compressor = Compressor(packager.pass_through, self.active_folder, compression_enabled)
-        self.stream_sha = return_hash_from_file(compressor.pass_through)
-        logging.info(f"SHA-256: {self.stream_sha}")
-        encryptor = Encryptor(compressor.pass_through, self.active_folder, crypto_key, scrypt_n, scrypt_r,
-                              scrypt_p)
-
-        self.encryption_enabled = encryptor.encryption_enabled
-        self.post_encryption_hash = None
-
-        if crypto_key:
-            self.post_encryption_hash = return_hash_from_file(encryptor.pass_through)
-            logging.debug(f'Post-encryption SHA-256: {self.post_encryption_hash}')
-
-        self.size_in_bytes = return_file_size(encryptor.pass_through)
-        logging.info(f'Package size: {self.size_in_bytes}B')
-        self.pass_through = encryptor.pass_through
-
-        logging.info("Preprocess complete.")
\ No newline at end of file
diff --git a/bitglitter/protocols/protocol_one/write/protocol_one_preprocessobjects.py b/bitglitter/protocols/protocol_one/write/protocol_one_preprocessobjects.py
deleted file mode 100644
index a9e4bc1..0000000
--- a/bitglitter/protocols/protocol_one/write/protocol_one_preprocessobjects.py
+++ /dev/null
@@ -1,88 +0,0 @@
-import logging
-import os
-
-from bitglitter.filepackager.filepackager import Folder, File, package
-from bitglitter.utilities.filemanipulation import compress_file, encrypt_file
-
-
-class Packager:
-    '''Packager object takes all files and folders from argument file_list in write, and begins assembling them into a
-    single package.dat file.  All arguments that aren't a file or folder are automatically discarded.
-    '''
-
-    def __init__(self, active_folder, file_list, mask_files):
-
-        self.file_list = file_list
-        root_folder = Folder(name='Root')
-        logging.info("Preparing files for packaging...")
-        invalid_input = False
-
-        # If a string was used to denote a single file or file path, this automatically converts it into a tuple.
-        if isinstance(file_list, str):
-            file_list = (file_list,)
-
-        for item in file_list:
-
-            if os.path.isfile(item):
-                logging.debug(f"Adding file: {item}")
-                new_object = File(item)
-                root_folder + new_object
-                invalid_input = True
-
-            if os.path.isdir(item):
-                logging.debug(f"Adding folder: {item}")
-                root_folder + Folder(file_path=item)
-                invalid_input = True
-
-        if invalid_input == False:
-            raise ValueError("No valid files or folders were found from your file_list tuple in write()")
-
-        self.pass_through = active_folder + '\\package.dat'
-
-        logging.info("Packaging files...")
-        package(root_folder, active_folder, mask_files)
-        logging.info("Packaging complete.")
-
-
-class Compressor:
-    '''Compressor takes package.dat created from Packager and compresses the file, assuming compression_enabled == True.
-    A hash of the file is taken as well at this point, if encryption_enabled == True.
-    '''
-
-    def __init__(self, pass_through, active_folder, compression_enabled):
-        self.pass_through = pass_through
-        self.pre_encryption_hash = ''
-
-        if compression_enabled == True:
-            logging.info('Compressing package...')
-            self.new_name = active_folder + '\\compressed.dat'
-
-            compress_file(self.pass_through, self.new_name)
-            self.pass_through = self.new_name
-            logging.info('Compression complete.')
-
-        else:
-            logging.info("Skipping compression.")
-
-
-class Encryptor:
-    '''Encryptor object applies encryption to the file being pre-processed if encryption_key has an argument.'''
-
-    def __init__(self, pass_through, active_folder, encryption_key, scrypt_n,
-                 scrypt_r, scrypt_p):
-        self.pass_through = pass_through
-        self._crypto_key = encryption_key
-
-        if len(encryption_key) == 0:
-            logging.info('Skipping encryption.')
-            self.encryption_enabled = False
-
-        else:
-            new_name = active_folder + '\\encrypted.dat'
-            logging.info('Encrypting package...')
-            encrypt_file(self.pass_through, new_name, encryption_key, scrypt_n, scrypt_r, scrypt_p)
-            logging.info('Encryption complete.')
-
-            logging.debug(f"Removed {self.pass_through}")
-            self.pass_through = new_name
-            self.encryption_enabled = True
\ No newline at end of file
diff --git a/bitglitter/protocols/protocol_one/write/protocol_one_renderassets.py b/bitglitter/protocols/protocol_one/write/protocol_one_renderassets.py
deleted file mode 100644
index 775a5d2..0000000
--- a/bitglitter/protocols/protocol_one/write/protocol_one_renderassets.py
+++ /dev/null
@@ -1,249 +0,0 @@
-import hashlib
-import logging
-import math
-import os
-import zlib
-
-from bitstring import BitArray, BitStream, ConstBitStream
-from PIL import ImageDraw
-
-from bitglitter.palettes.paletteutilities import palette_grabber, ValuesToColor
-
-
-def ascii_header_process(file_mask_enabled, active_path, stream_palette, bg_version, stream_name, stream_description,
-                         post_encryption_hash, encryption_enabled):
-    '''This takes all ASCII elements of the stream header, and returns a formatted merged string.'''
-
-    if file_mask_enabled:
-        file_list_string = ""
-
-    else:
-        with open(active_path + "\\file_list.txt", 'r') as text_file:
-            file_list_string = text_file.read()
-
-        os.remove(active_path + '\\file_list.txt')
-
-    custom_palette_string = ""
-    if stream_palette.palette_type == 'custom':
-        custom_palette_attribute_list = [stream_palette.name, stream_palette.description,
-                                      str(stream_palette.date_created), str(stream_palette.color_set)]
-        custom_palette_string = "\\\\".join(custom_palette_attribute_list) + "\\\\"
-
-    crypto_string = ""
-    if encryption_enabled:
-        crypto_string = post_encryption_hash + "\\\\"
-
-    meta_data_string = "\\\\" + "\\\\".join([bg_version, stream_name, stream_description, file_list_string]) + "\\\\"
-
-    merged_string = "".join([meta_data_string, custom_palette_string, crypto_string])
-    logging.debug(f'ASCII Stream Header merged: {merged_string}')
-
-    # Next, we're compressing it
-    compressed_stream_header = zlib.compress(merged_string.encode(), level=9)
-    compressed_file_size = len(compressed_stream_header)
-    logging.debug(f'ASCII Stream Header compressed. ({len(merged_string)} B -> {compressed_file_size} B)')
-
-    return compressed_stream_header
-
-
-def generate_stream_header_binary_preamble(size_in_bytes, total_frames, compression_enabled, encryption_enabled,
-                                           file_mask_enabled, is_stream_palette_custom, date_created, stream_palette_id,
-                                           ascii_compressed_length):
-    '''The binary preamble for the Stream Header is created here.  For videos and images, this is only needed for the
-    first frame.
-    '''
-
-    adding_bits = BitStream()
-
-    adding_bits.append(BitArray(uint=size_in_bytes, length=64))
-    adding_bits.append(BitArray(uint=total_frames, length=32))
-    adding_bits.append(BitArray([int(compression_enabled), int(encryption_enabled),
-                                 int(file_mask_enabled), is_stream_palette_custom]))
-    adding_bits.append(BitArray(uint=date_created, length=34))
-
-    if is_stream_palette_custom:
-        adding_bits.append(BitArray(hex=stream_palette_id))
-
-    else:
-        adding_bits.append(BitArray(uint=int(stream_palette_id), length=256))
-
-    adding_bits.append(BitArray(uint=ascii_compressed_length, length=32))
-
-    logging.debug('streamHeader generated.')
-    return ConstBitStream(adding_bits)
-
-
-def generate_frame_header(stream_sha, frame_hashable_bits, frame_number, blocks_used):
-    '''This creates the header that is present at the beginning of every frame (excluding the first frame or image
-    outputs).  These headers orient the reader, in that it tells it where it is in the stream.
-    '''
-
-    full_bit_string = BitArray()
-
-    full_bit_string.append(BitArray(hex=stream_sha))
-    temp_payload_holding = ConstBitStream(frame_hashable_bits)
-    sha_hasher = hashlib.sha256()
-    sha_hasher.update(temp_payload_holding.tobytes())
-    frame_sha = sha_hasher.digest()
-
-    full_bit_string.append(BitArray(bytes=frame_sha))
-    full_bit_string.append(BitArray(uint=frame_number, length=32))
-    full_bit_string.append(BitArray(uint=blocks_used, length=32))
-    full_bit_string_to_hash = full_bit_string.bytes
-
-    crc_output = zlib.crc32(full_bit_string_to_hash)
-    full_bit_string.append(BitArray(uint=crc_output, length=32))
-
-    return full_bit_string
-
-
-def how_many_frames(block_height, block_width, ascii_compressed_size, size_in_bytes, stream_palette, header_palette,
-                    output_mode):
-    '''This method returns how many frames will be required to complete the rendering process.'''
-
-    logging.debug("Calculating how many frames to render...")
-
-    total_blocks = block_height * block_width
-    stream_header_overhead_in_bits = 422 + (ascii_compressed_size * 8)
-    stream_size_in_bits = (size_in_bytes * 8)
-    header_bit_length = header_palette.bit_length
-    stream_bit_length = stream_palette.bit_length
-
-    # Overhead constants
-    initializer_overhead = block_height + block_width + 323
-    frame_header_overhead = 608
-
-    data_left = stream_size_in_bits
-    frame_number = 0
-    stream_header_bits_left = stream_header_overhead_in_bits  # subtract until zero
-
-    while stream_header_bits_left:
-
-        bits_consumed = frame_header_overhead
-        blocks_left = total_blocks
-        blocks_left -= (initializer_overhead * int(output_mode == 'image' or frame_number == 0))
-
-        stream_header_bits_available = (blocks_left * header_bit_length) - frame_header_overhead
-
-        if stream_header_bits_left >= stream_header_bits_available:
-            stream_header_bits_left -= stream_header_bits_available
-
-        else: # stream_header_combined terminates on this frame
-            stream_header_bits_available -= stream_header_bits_left
-            bits_consumed += stream_header_bits_left
-            stream_header_bits_left = 0
-
-            stream_header_blocks_used = math.ceil(bits_consumed / header_palette.bit_length)
-            attachment_bits = header_palette.bit_length - (bits_consumed % header_palette.bit_length)
-
-            if attachment_bits > 0:
-                data_left -= attachment_bits
-
-            remaining_blocks_left = blocks_left - stream_header_blocks_used
-            leftover_frame_bits = remaining_blocks_left * header_bit_length
-
-            if leftover_frame_bits > data_left:
-                data_left = 0
-
-            else:
-                data_left -= leftover_frame_bits
-
-        frame_number += 1
-
-    # Calculating how much data can be embedded in a regular frame_payload frame, and returning the total frame count
-    # needed.
-    blocks_left = total_blocks - (initializer_overhead * int(output_mode == 'image'))
-    payload_bits_per_frame = (blocks_left * stream_bit_length) - frame_header_overhead
-
-    total_frames = math.ceil(data_left / payload_bits_per_frame) + frame_number
-    logging.info(f'{total_frames} frame(s) required for this operation.')
-    return total_frames
-
-
-def generate_initializer(block_height, block_width, protocol_version, header_palette):
-    '''This generates the initializer header, which is present in black and white colors on the top of the first frame
-    of video streams, and every frame of image streams.  It provides import information on stream geometry as well as
-    protocol version.
-    '''
-
-    full_bit_string = BitArray()
-    full_bit_string.append(BitArray(uint=protocol_version, length=4))
-    full_bit_string.append(BitArray(uint=block_height, length=16))
-    full_bit_string.append(BitArray(uint=block_width, length=16))
-
-    if header_palette.palette_type == 'default':
-        full_bit_string.append(BitArray(uint=int(header_palette.id), length=256))
-
-    else:
-        full_bit_string.append(BitArray(hex=header_palette.id))
-
-    full_bit_string_to_hash = full_bit_string.tobytes()
-    crc_output = zlib.crc32(full_bit_string_to_hash)
-    full_bit_string.append(BitArray(uint=crc_output, length=32))
-
-    return full_bit_string
-
-
-def render_calibrator(image, block_height, block_width, pixel_width):
-    '''This creates the checkboard-like pattern along the top and left of the first frame of video streams, and every
-    frame of image streams.  This is what the reader uses to initially lock onto the frame.  Stream block_width and
-    block_height are encoded into this pattern, using alternating color palettes so no two repeating values produce a
-    continuous block of color, interfering with the frame lock process.'''
-
-    initializer_palette_dict_a = ValuesToColor(palette_grabber('1'), 'initializer_palette A')
-    initializer_palette_dict_b = ValuesToColor(palette_grabber('11'), 'initializer_palette B')
-
-    draw = ImageDraw.Draw(image)
-    draw.rectangle((0, 0, pixel_width - 1, pixel_width - 1),
-                   fill='rgb(0,0,0)')
-
-    block_width_encoded = BitArray(uint=block_width, length=block_width - 1)
-    block_width_encoded.reverse()
-    readable_block_width = ConstBitStream(block_width_encoded)
-
-    for i in range(block_width - 1):
-        next_bit = readable_block_width.read('bits : 1')
-
-        if i % 2 == 0:
-            color_value = initializer_palette_dict_b.get_color(ConstBitStream(next_bit))
-
-        else:
-            color_value = initializer_palette_dict_a.get_color(ConstBitStream(next_bit))
-
-        draw.rectangle((pixel_width * i + pixel_width,
-                        0,
-                        pixel_width * (i + 1) - 1 + pixel_width,
-                        pixel_width - 1),
-                       fill=f'rgb{str(color_value)}')
-
-    block_height_encoded = BitArray(uint=block_height, length=block_height - 1)
-    block_height_encoded.reverse()
-    readable_block_height = ConstBitStream(block_height_encoded)
-
-    for i in range(block_height - 1):
-        next_bit = readable_block_height.read('bits : 1')
-
-        if i % 2 == 0:
-            color_value = initializer_palette_dict_b.get_color(ConstBitStream(next_bit))
-
-        else:
-            color_value = initializer_palette_dict_a.get_color(ConstBitStream(next_bit))
-
-        draw.rectangle((0,
-                        pixel_width * i + pixel_width,
-                        pixel_width - 1,
-                        pixel_width * (i + 1) - 1 + pixel_width),
-                       fill=f'rgb{str(color_value)}')
-
-    return image
-
-
-def loop_generator(block_height, block_width, pixel_width, initializer_enabled):
-    '''This generator yields the coordinates for each of the blocks used, depending on the geometry of the frame.'''
-
-    for yRange in range(block_height - int(initializer_enabled)):
-        for xRange in range(block_width - int(initializer_enabled)):
-            yield ((pixel_width * int(initializer_enabled)) + (pixel_width * xRange),
-                   (pixel_width * int(initializer_enabled)) + (pixel_width * yRange),
-                   (pixel_width * int(initializer_enabled)) + (pixel_width * (xRange + 1) - 1),
-                   (pixel_width * int(initializer_enabled)) + (pixel_width * (yRange + 1) - 1))
\ No newline at end of file
diff --git a/bitglitter/protocols/protocol_one/write/protocol_one_renderlogic.py b/bitglitter/protocols/protocol_one/write/protocol_one_renderlogic.py
deleted file mode 100644
index e36881b..0000000
--- a/bitglitter/protocols/protocol_one/write/protocol_one_renderlogic.py
+++ /dev/null
@@ -1,131 +0,0 @@
-import logging
-
-from bitglitter.protocols.protocol_one.write.protocol_one_renderassets import ascii_header_process, how_many_frames
-from bitglitter.protocols.protocol_one.write.protocol_one_renderloop import render_loop
-from bitglitter.write.rendervideo import render_video
-
-class EncodeFrame:
-    '''This object takes what has already been processed from pre_processing, renders that data into images.'''
-
-    def __init__(self):
-
-        logging.debug('EncodeFrame initializing...')
-
-        self.PROTOCOL_VERSION = 1
-
-        self.active_path = None
-        self.pass_through = None
-        self.stream_output_path = None
-        self.bg_version = None
-
-        self.header_palette = None
-        self.initializer_palette = None
-        self.stream_palette = None
-
-        self.initializer_palette_dict = None
-        self.header_palette_dict = None
-        self.stream_palette_dict = None
-
-        self.block_height = None
-        self.block_width = None
-        self.pixel_width = None
-        self.frames_per_second = None
-        self.output_mode = None
-
-        self.stream_sha = None
-
-        # Stream header data to encode
-        self.size_in_bytes = None
-        self.total_frames = None
-        self.compression_enabled = None
-        self.encryption_enabled = None
-        self.file_mask_enabled = None
-        self.date_created = None
-        self.stream_name = None
-        self.stream_description = None
-        self.post_encryption_hash = None
-
-        self.total_frames = 0
-
-
-    def begin_session_process(self,
-
-                              # General- Path and Protocol
-                              active_path, pass_through, stream_output_path, bg_version,
-
-                              # Palettes
-                              initializer_palette, header_palette, stream_palette,
-
-                              # Color Dictionaries
-                              initializer_palette_dict, header_palette_dict, stream_palette_dict,
-
-                              # Geometry & General Render Configuration
-                              block_height, block_width, pixel_width, frames_per_second, output_mode,
-
-                              # Frame Header
-                              stream_sha,
-
-                              # Stream Header - Binary Preamble
-                              size_in_bytes, compression_enabled, encryption_enabled, file_mask_enabled,
-                              date_created,
-
-                              # Stream Header - ASCII Encoded
-                              stream_name, stream_description, pre_encryption_hash
-
-                              ):
-
-        #Load arguments
-        self.active_path = active_path
-        self.pass_through = pass_through
-        self.stream_output_path = stream_output_path
-        self.bg_version = bg_version
-
-        self.header_palette = header_palette
-        self.initializer_palette = initializer_palette
-        self.stream_palette = stream_palette
-
-        self.initializer_palette_dict = initializer_palette_dict
-        self.header_palette_dict = header_palette_dict
-        self.stream_palette_dict = stream_palette_dict
-
-        self.block_height = block_height
-        self.block_width = block_width
-        self.pixel_width = pixel_width
-        self.frames_per_second = frames_per_second
-        self.output_mode = output_mode
-
-        self.stream_sha = stream_sha
-
-        self.size_in_bytes = size_in_bytes
-        self.compression_enabled = compression_enabled
-        self.encryption_enabled = encryption_enabled
-        self.file_mask_enabled = file_mask_enabled
-        self.date_created = date_created
-
-        self.stream_name = stream_name
-        self.stream_description = stream_description
-        self.post_encryption_hash = pre_encryption_hash
-
-        self.ascii_compressed = ascii_header_process(self.file_mask_enabled, self.active_path,
-                                                     self.stream_palette, self.bg_version, self.stream_name,
-                                                     self.stream_description, self.post_encryption_hash,
-                                                     self.encryption_enabled)
-
-        self.total_frames = how_many_frames(self.block_height, self.block_width, len(self.ascii_compressed),
-                                            self.size_in_bytes, self.stream_palette, self.header_palette,
-                                            self.output_mode)
-
-        self.remainder_blocks, self.image_output_path, self.frame_number_formatted = render_loop(self.block_height,
-                                                        self.block_width, self.pixel_width, self.PROTOCOL_VERSION,
-                                                        self.initializer_palette, self.header_palette,
-                                                        self.stream_palette, self.output_mode, self.stream_output_path,
-                                                        self.active_path, self.pass_through, self.size_in_bytes,
-                                                        self.total_frames, self.compression_enabled,
-                                                        self.encryption_enabled, self.file_mask_enabled,
-                                                        self.date_created, self.ascii_compressed, self.stream_sha,
-                                                        self.initializer_palette_dict, self.header_palette_dict,
-                                                        self.stream_palette_dict)
-
-        if output_mode == 'video':
-            render_video(self.stream_output_path, self.date_created, self.image_output_path,
-                         self.frame_number_formatted, self.frames_per_second)
\ No newline at end of file
diff --git a/bitglitter/protocols/protocol_one/write/protocol_one_renderloop.py b/bitglitter/protocols/protocol_one/write/protocol_one_renderloop.py
deleted file mode 100644
index ad513b6..0000000
--- a/bitglitter/protocols/protocol_one/write/protocol_one_renderloop.py
+++ /dev/null
@@ -1,219 +0,0 @@
-import logging
-import math
-import time
-
-from bitstring import BitStream, ConstBitStream
-from PIL import Image, ImageDraw
-
-from bitglitter.protocols.protocol_one.write.protocol_one_renderassets import render_calibrator, generate_initializer, \
-    generate_frame_header, generate_stream_header_binary_preamble, loop_generator
-
-
-def render_loop(block_height, block_width, pixel_width, protocol_version, initializer_palette, header_palette,
-                stream_palette, output_mode, stream_output_path, active_path, pass_through, size_in_bytes, total_frames,
-                compression_enabled, encryption_enabled, file_mask_enabled, date_created, ascii_compressed, stream_sha,
-                initializer_palette_dict, header_palette_dict, stream_palette_dict):
-    '''This function iterates over the preProcessed data, and assembles and renders the frames.  There are plenty of
-    # comments in this function that describe what each part is doing, to follow along.
-    '''
-
-    logging.debug('Entering render_loop...')
-
-    # Determining output for images.
-    if output_mode == 'image':
-        if stream_output_path:
-            image_output_path = stream_output_path + '\\'
-
-        else:
-            image_output_path = ""
-
-    if output_mode == 'video':
-        image_output_path = active_path + '\\'
-
-    # Constants
-    TOTAL_BLOCKS = block_height * block_width
-    INITIALIZER_OVERHEAD = block_height + block_width + 323
-    INITIALIZER_DATA_BITS = 324
-    FRAME_HEADER_OVERHEAD = 608
-
-    active_payload = ConstBitStream(filename=pass_through)
-    frame_number = 1
-    primary_frame_palette_dict, primary_read_length = header_palette_dict, header_palette.bit_length
-    active_palette = header_palette
-    stream_palette_used = False
-    last_frame = False
-
-    # Final preparations for stream header parts.
-    stream_header_binary_preamble = generate_stream_header_binary_preamble(size_in_bytes, total_frames,
-                                                            compression_enabled, encryption_enabled, file_mask_enabled,
-                                                            stream_palette.palette_type == "custom", date_created,
-                                                            stream_palette.id, len(ascii_compressed))
-    stream_header_combined = BitStream(stream_header_binary_preamble)
-    stream_header_combined.append(ascii_compressed)
-
-    # This is the primary loop where all rendering takes place.  It'll continue until it traverses the entire file.
-    while active_payload.bitpos != active_payload.length:
-
-        logging.info(f'Rendering frame {frame_number} of {total_frames} ...')
-
-        # Setting up frame to draw on.
-        image = Image.new('RGB', (pixel_width * block_width, pixel_width * block_height), 'white')
-        draw = ImageDraw.Draw(image)
-
-        stream_header_chunk = BitStream()
-        payload_holder = BitStream()
-        attachment_bits_append = BitStream()
-        remainder_blocks_into_bits = BitStream()
-        bits_to_pad = BitStream()
-
-        max_allowable_payload_bits = len(active_payload) - active_payload.bitpos
-        bits_consumed = FRAME_HEADER_OVERHEAD
-        blocks_left = TOTAL_BLOCKS
-        initializer_enabled = False
-        blocks_used = 0
-        initializer_palette_blocks_used = 0
-        stream_header_blocks_used = 0
-
-        if stream_palette_used == True:
-            primary_frame_palette_dict, primary_read_length = stream_palette_dict, stream_palette.bit_length
-            active_palette = stream_palette
-
-        # Adding an initializer header if necessity.  initializer_enabled is a boolean that signals whether the
-        # initializer is on or not.
-        initializer_holder = BitStream()
-        if frame_number == 1 or output_mode == 'image':
-            image = render_calibrator(image, block_height, block_width, pixel_width)
-            initializer_holder = generate_initializer(block_height, block_width, protocol_version, active_palette)
-            initializer_palette_blocks_used += INITIALIZER_DATA_BITS
-            blocks_left -= INITIALIZER_OVERHEAD
-            initializer_enabled = True
-
-        bits_left_this_frame = (blocks_left * active_palette.bit_length) - FRAME_HEADER_OVERHEAD
-
-        # Here, we're calculating how many bits we can fit into the stream based on the palettes used.
-        # Normal frame_payload frames.
-        if stream_palette_used == True:
-
-            # Standard frame_payload frame in the middle of the stream.
-            if bits_left_this_frame <= max_allowable_payload_bits:
-                payload_holder = active_payload.read(bits_left_this_frame)
-
-            # Payload Frame terminates on this frame.
-            else:
-                logging.debug('Payload termination frame')
-                payload_holder = active_payload.read(max_allowable_payload_bits)
-                last_frame = True
-
-
-        # Frames that need stream_header_combined added to them.
-        else:
-
-            # This frame has more bits left for the streamHeader than capacity.
-            if len(stream_header_combined) - stream_header_combined.bitpos > bits_left_this_frame:
-                stream_header_chunk = stream_header_combined.read(bits_left_this_frame)
-                stream_header_blocks_used = math.ceil(len(stream_header_chunk + FRAME_HEADER_OVERHEAD)
-                                                   / active_palette.bitLength)
-
-
-            # stream_header_combined terminates on this frame
-            else:
-
-                logging.debug("Streamheader terminates this frame.")
-
-                stream_header_chunk = stream_header_combined.read(len(stream_header_combined)
-                                                                    - stream_header_combined.bitpos)
-                bits_left_this_frame -= len(stream_header_combined)
-                bits_consumed += len(stream_header_chunk)
-
-                stream_palette_used = True
-                stream_header_blocks_used = math.ceil(bits_consumed / active_palette.bit_length)
-
-                # There may be extra bits available at the end of the blocks used for the header_palette.  If that's
-                # the case, they will be calculated here.
-                final_block_partial_fill = bits_consumed % active_palette.bit_length
-
-                if final_block_partial_fill > 0:
-                    attachment_bits = active_palette.bit_length - (final_block_partial_fill)
-                    attachment_bits_append = active_payload.read(attachment_bits)
-
-                    bits_consumed += attachment_bits
-                    max_allowable_payload_bits -= attachment_bits
-
-                remaining_blocks_left = blocks_left - stream_header_blocks_used
-
-                # If there are remaining stream_palette blocks available for this frame, we go here.
-                if remaining_blocks_left > 0:
-                    bits_left_this_frame = remaining_blocks_left * active_palette.bit_length
-
-                    if max_allowable_payload_bits > bits_left_this_frame: # Payload continues on in next frame(s)
-                        remainder_blocks_into_bits = active_payload.read(bits_left_this_frame)
-
-                    else: # Full frame_payload can terminate on streamHeader termination frame.
-                        remainder_blocks_into_bits = active_payload.read(max_allowable_payload_bits)
-                        last_frame = True
-
-        frame_hashable_bits = stream_header_chunk + attachment_bits_append + remainder_blocks_into_bits + payload_holder
-        combined_frame_length = frame_hashable_bits.len + FRAME_HEADER_OVERHEAD
-        blocks_used = (int(initializer_enabled) * INITIALIZER_DATA_BITS) + math.ceil(combined_frame_length
-                                                                                   / active_palette.bit_length)
-
-        #On the last frame, there may be excess capacity in the final block.  This pads the payload as needed so it
-        #cleanly fits into the block.
-        if last_frame == True:
-
-            remainder_bits = active_palette.bit_length - (combined_frame_length % active_palette.bit_length)
-            if remainder_bits == active_palette.bit_length:
-                remainder_bits = 0
-
-            bits_to_pad = BitStream(bin=f"{'0' * remainder_bits}")
-            frame_hashable_bits.append(bits_to_pad)
-
-        frame_header_holder = generate_frame_header(stream_sha, frame_hashable_bits, frame_number, blocks_used)
-        combining_bits = initializer_holder + frame_header_holder + stream_header_chunk + attachment_bits_append \
-                        + remainder_blocks_into_bits + payload_holder + bits_to_pad
-
-        all_bits_to_write = ConstBitStream(combining_bits)
-        next_coordinates = loop_generator(block_height, block_width, pixel_width, initializer_enabled)
-        block_position = 0
-
-        # Drawing blocks to screen.
-        while len(all_bits_to_write) != all_bits_to_write.bitpos:
-
-            # Primary palette selection (ie, header_palette or stream_palette depending on where we are in the stream)
-            if block_position >= initializer_palette_blocks_used:
-                active_palette_dict, read_length = primary_frame_palette_dict, primary_read_length
-
-            # Initializer palette selection
-            elif block_position < initializer_palette_blocks_used:
-                active_palette_dict, read_length = (initializer_palette_dict, initializer_palette.bit_length)
-
-            # Here to signal something has broken.
-            else:
-                raise Exception('Something has gone wrong in matching block position to palette.  This state'
-                                '\nis reached only if something is broken.')
-
-            # This is loading the next bit(s) to be written in the frame, and then converting it to an RGB value.
-            next_bits = all_bits_to_write.read(f'bits : {read_length}')
-            color_value = active_palette_dict.get_color(ConstBitStream(next_bits))
-
-            # With the color loaded, we'll get the coordinates of the next block (each corner), and draw it in.
-            active_coordinates = next(next_coordinates)
-            draw.rectangle((active_coordinates[0], active_coordinates[1], active_coordinates[2], active_coordinates[3]),
-                           fill=f'rgb{str(color_value)}')
-
-            block_position += 1
-
-        # Frames get saved as .png files.
-        frame_number_to_string = str(frame_number)
-
-        if output_mode == 'video':
-            file_name = frame_number_to_string.zfill(math.ceil(math.log(total_frames + 1, 10)))
-
-        else:
-            file_name = time.strftime('%Y-%m-%d %H-%M-%S', time.localtime(date_created)) + ' - ' + str(frame_number)
-
-        image.save(f'{image_output_path}{str(file_name)}.png')
-        frame_number += 1
-
-    logging.debug('Render complete, running cleanup()...')
-    return block_position, image_output_path, str(frame_number)
\ No newline at end of file
diff --git a/bitglitter/protocols/protocol_one/write/protocol_one_verifywriteparameters.py b/bitglitter/protocols/protocol_one/write/protocol_one_verifywriteparameters.py
deleted file mode 100644
index b268b55..0000000
--- a/bitglitter/protocols/protocol_one/write/protocol_one_verifywriteparameters.py
+++ /dev/null
@@ -1,119 +0,0 @@
-import logging
-from math import ceil
-
-from bitglitter.config.config import config
-from bitglitter.utilities.generalverifyfunctions import is_bool, is_valid_directory, is_int_over_zero, \
-    proper_string_syntax
-
-def palette_verify(header_type, bit_length, block_width, block_height, output_type, fps=0):
-    '''This function calculates the necessary overhead for both images and videos for subsequent frames after 1.  It
-    returns a number between 0-100%, for what percentage the overhead occupies.  The lower the value, the higher the
-    frame efficiency.
-    '''
-
-    total_blocks = block_width * block_height
-
-    block_overhead = block_height + block_width + 323
-    frame_header_in_bits = 608
-
-    blocks_needed = 0
-
-    if header_type == 'header_palette' or output_type == 'image':
-        blocks_needed += block_overhead
-
-    bits_available = (total_blocks - blocks_needed) * bit_length
-    bits_available -= frame_header_in_bits
-    occupied_blocks = blocks_needed + ceil(frame_header_in_bits / bit_length)
-
-    if occupied_blocks > total_blocks:
-        raise ValueError(f"{header_type} has {occupied_blocks - total_blocks} too few blocks with this configuration."
-                         f"\nPlease fix this by using a palette with a larger bitlength or use more blocks per frame.")
-
-    output_per_sec = None
-    if output_type == 'video' and header_type == 'stream_palette':
-        output_per_sec = bits_available * fps
-
-    return ((round(100 - (occupied_blocks / total_blocks * 100), 2)), bits_available, output_per_sec)
-
-
-def verify_write_parameters(file_list, stream_name, stream_description, stream_output_path, output_mode,
-                            compression_enabled, file_mask_enabled, scrypt_n, scrypt_r, scrypt_p, stream_palette,
-                            header_palette, pixel_width, block_height, block_width, frames_per_second):
-    '''This function verifies all write() parameters for Protocol v1.  Look at this as the gatekeeper that stops
-    invalid arguments from proceeding through the process, potentially breaking the stream (or causing BitGlitter to
-    crash).
-    '''
-
-    logging.info("Verifying write parameters...")
-
-    if not file_list:
-        raise FileNotFoundError("A minimum of one file or folder is required for stream creation for argument file_list"
-                                " in write().")
-
-    # TEMPORARY until issue is fixed
-    if output_mode == 'video':
-        if header_palette == '24' or stream_palette == '24':
-            raise ValueError("24 bit palettes can not be used with videos at this time due to codec issues.  This is"
-                             "\nbeing worked on and will be restored soon!  This palette will still work on images.")
-
-
-    proper_string_syntax(stream_name, 'stream_name')
-    proper_string_syntax(stream_description, 'stream_description')
-
-    if stream_output_path:
-        is_valid_directory('stream_output_path', stream_output_path)
-
-    if output_mode != 'video' and output_mode != 'image':
-        raise ValueError("Argument output_mode in write() only accepts 'video' or 'image'.")
-
-    is_bool('compression_enabled', compression_enabled)
-    is_bool('file_mask_enabled', file_mask_enabled)
-
-    is_int_over_zero('scrypt_n', scrypt_n)
-    is_int_over_zero('scrypt_r', scrypt_r)
-    is_int_over_zero('scrypt_p', scrypt_p)
-
-    # is stream_palette valid?  We're simultaneously setting up a variable for a geometry just check below.
-    def does_palette_exist(palette, type):
-        if palette in config.color_handler.default_palette_list:
-            palette_to_return = config.color_handler.default_palette_list[palette]
-        elif palette in config.color_handler.custom_palette_list:
-            palette_to_return = config.color_handler.custom_palette_list[palette]
-        elif palette in config.color_handler.custom_palette_nickname_list:
-            palette_to_return = config.color_handler.custom_palette_nickname_list[palette]
-        else:
-            raise ValueError(f"Argument {type} in write() is not a valid ID or nickname.  Verify that exact value "
-                             "exists.")
-        return palette_to_return
-
-    active_header_palette = does_palette_exist(header_palette, 'header_palette')
-    active_stream_palette = does_palette_exist(stream_palette, 'stream_palette')
-
-    is_int_over_zero('pixel_width', pixel_width)
-    is_int_over_zero('block_height', block_height)
-    is_int_over_zero('block_width', block_width)
-    is_int_over_zero('frames_per_second', frames_per_second)
-
-    if frames_per_second != 30 and frames_per_second != 60:
-        raise ValueError("frames_per_second must either be 30 or 60 at this time (we're working on this!)")
-
-    if block_width < 17 or block_height < 17:
-        raise ValueError('Minimum block dimensions for Protocol 1 are 17 x 17.')
-
-    # With the given dimensions and bit length, is it sufficient?
-    returned_header_values = palette_verify('header_palette', active_header_palette.bit_length, block_width,
-                                            block_height, output_mode, frames_per_second)
-    logging.info(f'{returned_header_values[0]}% of the frame for initial header is allocated for frame payload (higher '
-                 f'is better)')
-
-    returned_stream_values = palette_verify('stream_palette', active_stream_palette.bit_length, block_width,
-                                            block_height, output_mode, frames_per_second)
-    logging.info(f'{returned_stream_values[1]} B, or {returned_stream_values[0]}% of subsequent frames is allocated for'
-                 f' frame payload (higher is better)')
-
-    if output_mode == 'video':
-        logging.info(f'As a video, it will effectively be transporting {round(returned_stream_values[2] / 8)} B/sec of'
-                     f' data.')
-
-    logging.info('Minimum geometry requirements met.')
-    logging.info("Write parameters validated.")
\ No newline at end of file
diff --git a/bitglitter/protocols/protocolhandler.py b/bitglitter/protocols/protocolhandler.py
deleted file mode 100644
index bfd866a..0000000
--- a/bitglitter/protocols/protocolhandler.py
+++ /dev/null
@@ -1,38 +0,0 @@
-import logging
-
-import bitglitter.protocols.protocolversions as protocolversions
-
-class ProtocolHandler:
-    '''This is the master protocol object that holds Protocol 1, and will hold future protocols down the road.'''
-
-    def __init__(self):
-
-        logging.debug('Protocol handler initializing...')
-
-        self.available_protocols = {}
-        self.available_write_protocols = {}
-        self.available_read_protocols = {}
-
-
-    def _accept_new_protocol(self, protocol):
-        '''Internal method that takes protocols and loads them into the above dictionaries.'''
-
-        self.available_protocols[protocol.version_number] = protocol
-        self.available_write_protocols[protocol.version_number] = protocol.write
-        self.available_read_protocols[protocol.version_number] = protocol.read
-
-
-    def return_read_protocol(self, version_key):
-        '''This returns the read protocol object.'''
-
-        return self.available_read_protocols[version_key]
-
-
-    def return_write_protocol(self, version_key):
-        '''This returns the collection of write protocol objects to render a stream.'''
-
-        return self.available_write_protocols[version_key]
-
-# This is where new protocols get added.
-protocol_handler = ProtocolHandler()
-protocol_handler._accept_new_protocol(protocolversions.protocolOne)
\ No newline at end of file
diff --git a/bitglitter/protocols/protocolobjects.py b/bitglitter/protocols/protocolobjects.py
deleted file mode 100644
index 9a9ef88..0000000
--- a/bitglitter/protocols/protocolobjects.py
+++ /dev/null
@@ -1,49 +0,0 @@
-class Protocol:
-    '''Protocol objects are a container for what other funtionality is needed to carry out processing in Bitglitter.
-    Those objects are stored in two sub-objects, ProtocolWrite and ProtocolRead.
-    '''
-
-    def __init__(self,
-
-                 # Required values; it's lookup key, how data is converted, and how frames are decoded back into data.
-                 version_number,
-                 frame_processor,
-                 decoder,
-
-                 # Optional write process steps.
-                 verify_write_params_function = None,
-                 pre_processing = None,
-                 initializer_used = None,
-
-                 # Optional read process steps.
-                 post_processing = None
-                 ):
-
-        self.version_number = version_number
-
-        self.write = ProtocolWrite(version_number, verify_write_params_function, pre_processing, frame_processor,
-                                   initializer_used, )
-        self.read = ProtocolRead(decoder, post_processing)
-
-
-class ProtocolWrite:
-    '''Holds objects needed to perform a write for the Protocol object it is instantiated in.'''
-
-    def __init__(self, versionNumber, verifyWriteParamsFunction, preProcessing, frameProcessor, initializerUsed):
-
-        self.version_number = versionNumber
-
-        self.verify_write_parameters = verifyWriteParamsFunction
-        self.pre_processing = preProcessing
-        self.frame_processor = frameProcessor
-
-        self.initializer_used = initializerUsed
-
-
-class ProtocolRead:
-    '''Holds objects needed to read a stream for the Protocol object it is instantiated in.'''
-
-    def __init__(self, decoder, postProcessing):
-
-        self.decoder = decoder
-        self.post_processing = postProcessing
\ No newline at end of file
diff --git a/bitglitter/protocols/protocolversions.py b/bitglitter/protocols/protocolversions.py
deleted file mode 100644
index c8cc462..0000000
--- a/bitglitter/protocols/protocolversions.py
+++ /dev/null
@@ -1,13 +0,0 @@
-import bitglitter.protocols.protocol_one.write.protocol_one_preprocess as protocol_one_preprocess
-import bitglitter.protocols.protocol_one.write.protocol_one_renderlogic as protocol_one_renderlogic
-import bitglitter.protocols.protocol_one.write.protocol_one_verifywriteparameters as protocol_one_verifywriteparameters
-from bitglitter.protocols.protocolobjects import Protocol
-
-#This is where protocol objects get instantiated from the various components it comprises of.
-protocolOne = Protocol('1',
-                       protocol_one_renderlogic.EncodeFrame,
-                       "decoder",
-                       verify_write_params_function=protocol_one_verifywriteparameters.verify_write_parameters,
-                       pre_processing=protocol_one_preprocess.PreProcessor,
-                       initializer_used="initializerProtocolOne"
-                       )
\ No newline at end of file
diff --git a/bitglitter/read/__init__.py b/bitglitter/read/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/bitglitter/read/assembler.py b/bitglitter/read/assembler.py
deleted file mode 100644
index 3c5600f..0000000
--- a/bitglitter/read/assembler.py
+++ /dev/null
@@ -1,97 +0,0 @@
-import logging
-import shutil
-
-from bitglitter.config.constants import READ_PATH
-from bitglitter.read.partialsave import PartialSave
-
-
-class Assembler:
-    '''This object holds PartialSave objects, which holds the state of the file being read until it is fully assembled
-    and complete, which then purges the record.
-    '''
-
-    def __init__(self):
-        self.working_folder = READ_PATH
-        self.save_dict = {}
-        self.active_session_hashes = []
-
-
-    def check_if_frame_needed(self, stream_sha, frame_number):
-
-        if stream_sha not in self.save_dict:
-            return True
-        else:
-            if self.save_dict[stream_sha].is_frame_needed(frame_number) == False:
-                logging.info(f'Frame # {frame_number} / {self.save_dict[stream_sha].total_frames} for stream SHA '
-                             f'{stream_sha} is already saved!  Skipping...')
-            else:
-                return True
-        return False
-
-
-    def create_partial_save(self, stream_sha, scrypt_n, scrypt_r, scrypt_p, output_path, encryption_key, assemble_hold):
-        self.save_dict[stream_sha] = PartialSave(stream_sha, self.working_folder, scrypt_n, scrypt_r, scrypt_p, output_path,
-                                                 encryption_key, assemble_hold)
-
-
-    def save_frame_into_partial_save(self, stream_sha, payload, frame_number):
-        logging.info(f'Frame # {frame_number} / {self.save_dict[stream_sha].total_frames} for stream SHA {stream_sha} '
-                     f'saved!')
-        self.save_dict[stream_sha].load_frame_data(payload, frame_number)
-
-
-    def accept_frame(self, stream_sha, payload, frame_number, scrypt_n, scrypt_r, scrypt_p, output_path, encryption_key,
-                     assemble_hold):
-        '''This method accepts a validated frame.'''
-
-        if stream_sha not in self.save_dict:
-            self.create_partial_save(stream_sha, scrypt_n, scrypt_r, scrypt_p, output_path, encryption_key, assemble_hold)
-
-        if stream_sha not in self.active_session_hashes:
-            self.active_session_hashes.append(stream_sha)
-
-        self.save_frame_into_partial_save(stream_sha, payload, frame_number)
-
-
-    def review_active_sessions(self):
-        '''This method will go over all stream_sha's that were read in this read session, and will check to see if
-        check if frames_ingested == total_frames AND the frame reference table is displaying all frames are present.  This
-        only runs if there is at least one active session.
-        '''
-
-        if self.active_session_hashes:
-            logging.info('Reviewing active sessions and attempting assembly...')
-            for partial_save in self.active_session_hashes:
-
-                # Not ready to be assembled this session.
-                if self.save_dict[partial_save]._attempt_assembly() == False \
-                        and self.save_dict[partial_save].assemble_hold == False:
-                    self.save_dict[partial_save]._close_session()
-
-                # Assembled, temporary files pending deletion.
-                else:
-                    logging.info(f'{partial_save} fully read!  Deleting temporary files...')
-                    self.remove_partial_save(partial_save)
-
-            self.active_session_hashes = []
-
-
-    def remove_partial_save(self, stream_sha):
-        '''Removes PartialSave from dictionary.  Is used either through direct user argument, or by read() once a stream
-        is successfully converted back into a file.
-        '''
-
-        del self.save_dict[stream_sha]
-        shutil.rmtree(self.working_folder + f'\\{stream_sha}')
-        logging.debug(f'Temporary files successfully removed.')
-
-
-    def clear_partial_saves(self):
-        '''This removes all save data.  Called by user function remove_all_partial_saves() in savedfilefunctions.'''
-
-        try:
-            shutil.rmtree(self.working_folder)
-        except:
-            pass
-
-        self.save_dict = {}
\ No newline at end of file
diff --git a/bitglitter/read/coloranalysis.py b/bitglitter/read/coloranalysis.py
deleted file mode 100644
index 3ea6ce4..0000000
--- a/bitglitter/read/coloranalysis.py
+++ /dev/null
@@ -1,20 +0,0 @@
-import math
-
-def color_snap(raw_frame_rgb, palette_color_list):
-
-    closest_match = None
-    closest_distance = 500
-    for color in palette_color_list:
-
-        active_distance = math.sqrt(((raw_frame_rgb[0] - color[0]) ** 2) + ((raw_frame_rgb[1] - color[1]) ** 2)
-                                   + ((raw_frame_rgb[2] - color[2]) ** 2))
-        if active_distance < closest_distance:
-            closest_match = color
-            closest_distance = active_distance
-    return closest_match
-
-
-def return_distance(raw_frame_rgb, expected_value):
-    return math.sqrt(((raw_frame_rgb[0] - expected_value[0]) ** 2) + ((raw_frame_rgb[1] - expected_value[1]) ** 2) +
-                     ((raw_frame_rgb[2] - expected_value[2]) ** 2))
-
diff --git a/bitglitter/read/decode/__init__.py b/bitglitter/read/decode/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/bitglitter/read/decode/headerdecode.py b/bitglitter/read/decode/headerdecode.py
new file mode 100644
index 0000000..0834381
--- /dev/null
+++ b/bitglitter/read/decode/headerdecode.py
@@ -0,0 +1,188 @@
+import ast
+from datetime import datetime
+import logging
+
+from bitglitter.config.config import session
+from bitglitter.config.configmodels import Constants
+from bitglitter.config.palettemodels import Palette
+from bitglitter.read.decode.headerutilities import crc_verify
+from bitglitter.utilities.compression import decompress_bytes
+from bitglitter.utilities.cryptography import decrypt_bytes, get_sha256_hash_from_bytes
+
+
+def initializer_header_validate_decode(bit_stream, block_height_estimate, block_width_estimate):
+    """This decodes the raw binary data from the initializer header after verifying its CRC checksum, and will
+    stop the read if any of these conditions are met:  If the read checksum differs from the calculated checksum, if the
+    read protocol version isn't supported by this BitGlitter version, if the decoded block height or block width differ
+    from what the frame lockon read, or if the there is a default palette used which isn't recognized by this version of
+    BitGlitter.  Returns either 'failure' messages (see variables directly above), or the loaded stream palette.
+    """
+
+    # First, we're verifying the initializer is not corrupted by comparing its read checksum with a calculated one from
+    # it's contents.  If they match, we continue.  If not, this frame aborts.
+
+    logging.debug('initializer_header_decode running...')
+    assert len(bit_stream.bin) == 580
+
+    if not crc_verify(bit_stream):
+        logging.warning('Initializer checksum failure.  Aborting...')
+        return False
+
+    bit_stream.pos = 0
+    protocol_version = bit_stream.read('uint : 4')
+    logging.debug(f'Protocol version: {protocol_version}')
+    constants = session.query(Constants).first()
+    if str(protocol_version) not in constants.return_supported_protocols():
+        logging.warning(f'Protocol v{str(protocol_version)} not supported in v{constants.BG_VERSION} of BitGlitter.'
+                        f'  Please update to fix.  Aborting...')
+        return False
+
+    decoded_block_height = bit_stream.read('uint : 16')
+    decoded_block_width = bit_stream.read('uint : 16')
+    if decoded_block_height != block_height_estimate or decoded_block_width != block_width_estimate:
+        logging.warning('Geometry assertion failure.  This occurs from corrupted header data, or when frame scanning'
+                        ' cannot properly read the calibrator.  Aborting...')
+        logging.debug(f'decoded_block_height: {decoded_block_height} block_height_estimate: {block_height_estimate}'
+                      f'\ndecoded_block_width: {decoded_block_width} block_width_estimate: {block_width_estimate}')
+        return False
+
+    bit_stream.pos += 192
+    stream_palette_id = bit_stream.read('uint : 64')
+
+    #  Custom palette used
+    if stream_palette_id > 100:
+        custom_palette_used = True
+        bit_stream.pos -= 256
+        stream_palette_id = bit_stream.read('hex : 256')
+        stream_palette_id.lower()
+        palette = session.query(Palette).filter(Palette.palette_id == stream_palette_id).first()
+        if palette is None:
+            logging.debug('Unknown custom palette detected, will receive palette data in several headers from now.')
+        else:
+            logging.info(f'Known custom palette \'{palette.name}\' detected.')
+
+    #  Default palette used
+    else:
+        custom_palette_used = False
+        palette = session.query(Palette).filter(Palette.palette_id == stream_palette_id).first()
+        if palette is None:
+            logging.warning('This default palette is unknown by this version of BitGlitter.  This could be the case '
+                            'if you\'re using an older version.  Aborting...')
+            logging.debug(f'stream_palette_id: {stream_palette_id}')
+            return False
+        else:
+            logging.info(f'Default palette \'{palette.name}\' detected.')
+
+    # Get Stream SHA-256
+    stream_sha256 = bit_stream.read('hex : 256')
+
+    return {'palette': palette, 'stream_sha256': stream_sha256, 'protocol_version': protocol_version,
+            'stream_palette_id': stream_palette_id, 'custom_palette_used': custom_palette_used}
+
+
+def frame_header_decode(bit_stream):
+    """While read_initializer is mostly used for validation of values, this function's purpose is to return values
+    needed for the reading process, once verified.  Returns stream_sha, frame_sha, frame_number, and blocks_to_read.
+    """
+
+    logging.debug('frame_header_decode running...')
+    assert len(bit_stream.bin) == 352
+
+    if not crc_verify(bit_stream):
+        logging.debug(f'Frame header checksum failure.')
+        return False
+
+    bit_stream.pos = 0
+    bits_to_read = bit_stream.read('uint : 32')
+    frame_number = bit_stream.read('uint : 32')
+    frame_sha256 = bit_stream.read('hex : 256')
+
+    logging.debug('frame_header_decode successfully ran.')
+
+    return {'frame_sha256': frame_sha256, 'frame_number': frame_number, 'bits_to_read': bits_to_read}
+
+
+def stream_header_decode(bit_stream):
+    """This function takes the raw bit string taken from the frame(s) and extracts stream data from it."""
+
+    logging.debug('stream_header_decode running...')
+    assert len(bit_stream.bin) == 685
+
+    if not crc_verify(bit_stream):
+        logging.warning('Stream header checksum failure!  Aborting...')
+        return False
+
+    bit_stream.pos = 0
+    size_in_bytes = bit_stream.read('uint : 64')
+    total_frames = bit_stream.read('uint : 32')
+    compression_enabled = bit_stream.read('bool')
+    encryption_enabled = bit_stream.read('bool')
+    file_masking_enabled = bit_stream.read('bool')
+
+    metadata_header_length = bit_stream.read('uint : 32')
+    metadata_header_hash = bit_stream.read('hex : 256')
+    custom_palette_header_length = bit_stream.read('uint : 10')
+    custom_palette_header_hash = bit_stream.read('hex : 256')
+
+    return {'size_in_bytes': size_in_bytes, 'total_frames': total_frames, 'compression_enabled': compression_enabled,
+            'encryption_enabled': encryption_enabled, 'file_masking_enabled': file_masking_enabled,
+            'metadata_header_length': metadata_header_length, 'metadata_header_hash': metadata_header_hash,
+            'custom_palette_header_length': custom_palette_header_length, 'custom_palette_header_hash':
+                custom_palette_header_hash}
+
+
+def metadata_header_validate_decode(header_bytes, read_sha256, crypto_key, encryption_enabled, file_mask_enabled,
+                                    scrypt_n=14, scrypt_r=8, scrypt_p=1, frame_processor=True):
+    logging.debug('metadata_header_decode running...')
+
+    #  Attempt decryption if encryption enabled
+    decrypted_bytes = None
+    if encryption_enabled and file_mask_enabled:
+        decrypted_bytes = decrypt_bytes(header_bytes, crypto_key, scrypt_n, scrypt_r, scrypt_p)
+        if decrypted_bytes:
+            logging.info('Successful decryption.')
+        else:  # Signalling there was a decryption failure
+            logging.warning('Provided decryption values (key, scrypt_n, scrypt_r, scrypt_p) invalid.  Cannot extract'
+                            ' files until correct parameters received.')
+            return None
+
+    decompressed_bytes = decompress_bytes(decrypted_bytes if encryption_enabled and file_mask_enabled else header_bytes)
+
+    #  Integrity check
+    if frame_processor:
+        calculated_sha256 = get_sha256_hash_from_bytes(decompressed_bytes)
+        if calculated_sha256 != read_sha256:  # Shouldn't be possible but here nonetheless to ensure data integrity.
+            logging.warning('Read Stream SHA-256 does not match calculated.  This should never (in theory) be seen'
+                            'from the other layers of integrity checks.  Aborting...')
+            return False
+
+    decoded = decompressed_bytes.decode()
+    bg_version, stream_name, stream_description, time_created, manifest_string = decoded.split('\\\\')
+    time_created = int(time_created)
+    time_datetime = datetime.fromtimestamp(time_created)
+    logging.info(f"Metadata successfully decoded:\nStream name: {stream_name}\nStream description: "
+                 f"{stream_description}\nTime created: {time_datetime.strftime('%b %d, %Y %I:%M:%S %p')}")
+    return {'bg_version': bg_version, 'stream_name': stream_name, 'stream_description': stream_description,
+            'time_created': time_created, 'manifest_string': manifest_string}
+
+
+def custom_palette_header_validate_decode(header_bytes, read_sha256):
+    logging.debug('custom_palette_header_decode running...')
+    decompressed_bytes = decompress_bytes(header_bytes)
+
+    #  Integrity check
+    calculated_sha256 = get_sha256_hash_from_bytes(decompressed_bytes)
+    if calculated_sha256 != read_sha256:  # Shouldn't be possible but here nonetheless to ensure data integrity.
+        logging.warning('Read palette header SHA-256 does not match calculated.  This should never (in theory) be seen'
+                        ' from the other layers of integrity checks.  Aborting...')
+        return False
+    palette_id, palette_name, palette_description, time_created, number_of_colors, color_list = \
+        decompressed_bytes.decode().split('\\\\')
+    time_created = int(time_created)
+    number_of_colors = int(number_of_colors)
+    color_list = ast.literal_eval(color_list)  # Revisit in future.  Safer way?
+    if isinstance(color_list, list) or isinstance(color_list, tuple):
+        return {'palette_id': palette_id, 'palette_name': palette_name, 'palette_description': palette_description,
+                'time_created': time_created, 'number_of_colors': number_of_colors, 'color_list': color_list}
+    else:
+        raise ValueError('Emergency stop- returned string was not a list or tuple.')
diff --git a/bitglitter/read/decode/headerutilities.py b/bitglitter/read/decode/headerutilities.py
new file mode 100644
index 0000000..3f4c391
--- /dev/null
+++ b/bitglitter/read/decode/headerutilities.py
@@ -0,0 +1,25 @@
+from zlib import crc32
+
+from bitglitter.utilities.cryptography import get_sha256_hash_from_bytes
+
+
+def crc_verify(header):
+    """This function takes in the payload, takes the CRC32 of it, then compares it to what
+    is encoded.  Will return True or False depending on whether it matches.
+    """
+
+    header.pos = 0
+    hashable_bits = header.read(f'bits : {len(header.bin) - 32}')
+    converted_to_bytes = hashable_bits.tobytes()
+    calculated_crc = crc32(converted_to_bytes)
+    read_crc = header.read('uint : 32')
+    return calculated_crc == read_crc
+
+
+def sha256_verify(header, read_sha256):
+    """Does the same as crc_verify, except with SHA-256, and an externally supplied hash (from stream header)"""
+    header.pos = 0
+    hashable_bits = header.read(f'bits : {len(header.bin) - 256}')
+    converted_to_bytes = hashable_bits.tobytes()
+    calculated_sha256 = get_sha256_hash_from_bytes(converted_to_bytes, byte_output=False)
+    return calculated_sha256 == read_sha256
diff --git a/bitglitter/read/decode/manifest.py b/bitglitter/read/decode/manifest.py
new file mode 100644
index 0000000..96779d9
--- /dev/null
+++ b/bitglitter/read/decode/manifest.py
@@ -0,0 +1,48 @@
+from bitglitter.config.readmodels.readmodels import StreamFile
+
+
+def manifest_unpack(manifest_dict, stream_id, save_directory, bit_index=0, file_sequence=1):
+    """Takes a manifest dictionary and unpacks it into file models, and calculates their byte index within the greater
+    payload.
+    """
+
+    def file_process(file_dict, file_sequence, bit_index, save_directory, stream_id):
+
+        if 'ps' in file_dict:
+            payload_bit_size = file_dict['ps'] * 8
+        else:
+            payload_bit_size = file_dict['rs'] * 8
+
+        start_bit_position = bit_index
+        end_bit_position = bit_index + payload_bit_size - 1
+
+        save_path = save_directory / file_dict['fn']
+
+        StreamFile.create(stream_id=stream_id, sequence=file_sequence, start_bit_position=start_bit_position,
+                          end_bit_position=end_bit_position, save_path=str(save_path),
+                          raw_file_size_bytes=file_dict['rs'], raw_file_hash=file_dict['rh'],
+                          processed_file_size_bytes=file_dict['ps'] if 'ps' in file_dict else None,
+                          processed_file_hash=file_dict['ph'] if 'ph' in file_dict else None)
+
+    file_sequence = file_sequence
+    bit_index = bit_index
+
+    if 'fn' not in manifest_dict:  # Root is directory
+        if 'f' in manifest_dict:
+            for file in manifest_dict['f']:
+                file_process(file, file_sequence, bit_index, save_directory, stream_id)
+                file_sequence += 1
+                if 'ps' in file:
+                    bit_index += file['ps'] * 8
+                else:
+                    bit_index += file['rs'] * 8
+        if 's' in manifest_dict:
+            for subdirectory_dict in manifest_dict['s']:
+                bit_index, file_sequence = manifest_unpack(subdirectory_dict, stream_id,
+                                                           save_directory=save_directory / subdirectory_dict['n'],
+                                                           bit_index=bit_index, file_sequence=file_sequence)
+
+    else:  # Root is single file
+        file_process(manifest_dict, file_sequence, bit_index, save_directory, stream_id)
+
+    return bit_index, file_sequence
diff --git a/bitglitter/read/decoder.py b/bitglitter/read/decoder.py
deleted file mode 100644
index d67d550..0000000
--- a/bitglitter/read/decoder.py
+++ /dev/null
@@ -1,290 +0,0 @@
-import logging
-
-from bitglitter.palettes.paletteutilities import palette_grabber, ColorsToValue, _validate_and_add_palette
-from bitglitter.read.decoderassets import minimum_block_checkpoint, read_frame_header, read_initializer, validate_payload
-from bitglitter.read.framehandler import FrameHandler
-from bitglitter.read.framelockon import frame_lock_on
-
-
-class Decoder:
-    '''The Decoder object is what ultimately handles all higher level processing of each BitGlitter frame, orchestrating
-    lower-level behavior.  After frames are locked onto, and information is validated as not corrupt (or not needed,
-    as the frames may have already been previously read), that data is then passed onto the Assembler.
-    '''
-
-    def __init__(self, is_video, config_object, scrypt_n, scrypt_r, scrypt_p, block_height_override, block_width_override,
-                 output_path, encryption_key, assemble_hold):
-
-        # Misc Setup
-        self.is_video = is_video
-        self.frame_number_of_video = 0
-        self.active_frame = None
-        self.frame_height = None
-        self.frame_width = None
-        self.checkpoint_passed = True
-        self.fatal_checkpoint = True # Non-recoverable errors that require an immediate break from the loop.
-        self.stream_header_cleared = False
-        self.duplicate_frame_read = False
-
-        # Input arguments
-        self.block_height_override = block_height_override
-        self.block_width_override = block_width_override
-        self.scrypt_n = scrypt_n
-        self.scrypt_r = scrypt_r
-        self.scrypt_p = scrypt_p
-        self.encryption_key = encryption_key
-        self.output_path = output_path
-        self.assemble_hold = assemble_hold
-
-        # Lock on Characteristics
-        self.pixel_width = None
-        self.block_height = None
-        self.block_width = None
-        self.protocol_version = None
-
-        # Palette Variables
-        self.initializer_palette = palette_grabber('1')
-        self.initializer_palette_dict = ColorsToValue(self.initializer_palette)
-        self.primary_palette = None
-        self.primary_palette_dict = None
-        self.header_palette = None
-        self.header_palette_dict = None
-        self.stream_palette = None
-        self.stream_palette_dict = None
-
-        # Custom color data used in instantiating new palette
-        self.custom_color_name = None
-        self.custom_color_description = None
-        self.custom_color_date_created = None
-        self.custom_color_color_set = None
-
-        # Frame Data
-        self.stream_sha = None
-        self.frame_sha = None
-        self.stream_sha_from_last_frame = None
-        self.frame_number_of_stream = None
-        self.frame_payload = None
-        self.carry_over_bits = None
-        self.blocks_to_read = None
-
-        # Auxiliary Components
-        self.config_object = config_object
-        self.frame_handler = FrameHandler(self.initializer_palette, self.initializer_palette_dict)
-
-
-    def decode_image(self, file_to_input):
-        '''This method is used to decode a single image (jpg, png, bmp).'''
-
-        self.active_frame = file_to_input
-        self.frame_height, self.frame_width, unused = self.active_frame.shape
-        self.frame_handler.load_new_frame(self.active_frame, True)
-
-        if self._first_frame_setup() == False:
-            return False
-
-        if self._image_frame_setup() == False:
-            return False
-
-        if self._frame_validation('primary_palette') == False:
-            return False
-
-        if self._payload_process('primary_palette') == False:
-            return False
-
-        if self.config_object.assembler.save_dict[self.stream_sha].stream_header_ascii_complete == True \
-                and self.config_object.assembler.save_dict[self.stream_sha].stream_palette_read == False:
-            self._attempt_stream_palette_load()
-
-
-    def decode_video_frame(self, active_frame):
-        '''This is the higher level method that decodes and validates data from each video frame, and then passes it to
-        the Assembler object for further processing.
-        '''
-
-        self.frame_number_of_video += 1
-        self.frame_number_of_stream = None
-        self.frame_sha = None
-        self.frame_payload = None
-        self.carry_over_bits = None
-        self.duplicate_frame_read = False
-
-        self.active_frame = active_frame
-        self.frame_height, self.frame_width, unused = self.active_frame.shape
-
-        if self.frame_number_of_video == 1:
-            self.frame_handler.load_new_frame(self.active_frame, True)
-
-            if self._first_frame_setup() == False:
-                return False
-
-            if self._video_first_frame_setup() == False:
-                return False
-
-        else:
-            self.frame_handler.load_new_frame(self.active_frame, False)
-
-            if self.stream_header_cleared == False:
-                self._attempt_stream_palette_load()
-
-                if self.fatal_checkpoint == False:
-                    return False
-
-        if self.stream_header_cleared == False: # We are still on header_palette frames.
-            if self._frame_validation('header_palette') == False:
-                return False
-
-            if self._payload_process('header_palette') == False:
-                return False
-
-        else: # It has switched to stream_palette frames.
-            if self._frame_validation('stream_palette') == False:
-                return False
-
-            if self._payload_process('stream_palette') == False:
-                return False
-
-
-    def _first_frame_setup(self):
-        '''This is a series of tasks that must be done for the first frames of both video and images.  The frame is
-        locked onto, and the initializer from the frame is validated and loaded.
-        '''
-
-        self.checkpoint_passed = minimum_block_checkpoint(self.block_height_override, self.block_width_override,
-                                                          self.frame_width, self.frame_height)
-        if self.checkpoint_passed == False:
-            return False
-
-        self.block_height = self.block_height_override
-        self.block_width = self.block_width_override
-
-        self.block_height, self.block_width, self.pixel_width = frame_lock_on(self.active_frame, self.block_height_override,
-                                                                              self.block_width_override, self.frame_width,
-                                                                              self.frame_height)
-        if self.pixel_width == False:
-            return False
-
-        self.frame_handler.update_scan_geometry(self.block_height, self.block_width, self.pixel_width)
-
-
-    def _frame_validation(self, palette_type):
-        '''This internal method first validates the frame by checking the frame header, and then checks with the
-        Assembler object to see if this frame is needed.  This is ran on every frame.
-        '''
-
-        self.frameHeaderBits, self.carry_over_bits = self.frame_handler.return_frame_header(palette_type)
-
-        self.stream_sha, self.frame_sha, self.frame_number_of_stream, self.blocks_to_read = \
-            read_frame_header(self.frameHeaderBits)
-
-        if self.stream_sha == False:
-            return False
-
-        self.stream_sha_from_last_frame = self.stream_sha
-        self.frame_handler.update_blocks_to_read(self.blocks_to_read)
-
-        if self.config_object.assembler.check_if_frame_needed(self.stream_sha, self.frame_number_of_stream) == False:
-            self.duplicate_frame_read = True
-            if self.is_video == False:
-                self.config_object.assembler.save_dict[self.stream_sha]._close_session()
-
-            return False
-
-
-    def _payload_process(self, palette_type):
-        '''If the frame is validated, this method appends any carry-overy bits from reading the frame handler blocks,
-        and then passes it on into the Assembler.  This is ran on every frame.
-        '''
-
-        self.carry_over_bits.append(self.frame_handler.return_remainder_payload(palette_type))
-        self.frame_payload = self.carry_over_bits
-
-        if validate_payload(self.frame_payload, self.frame_sha) == False:
-            return False
-
-        self.config_object.assembler.accept_frame(self.stream_sha, self.frame_payload, self.frame_number_of_stream,
-                                                  self.scrypt_n, self.scrypt_r, self.scrypt_p, self.output_path,
-                                                  self.encryption_key, self.assemble_hold)
-
-
-    def _attempt_stream_palette_load(self):
-        '''The stream header is where the data stating the stream palette used is stated.
-        Commonly this will be able to be read on the first frame, but for larger stream headers using custom palettes,
-        this may take several frames for it to successfully load, since the full compressed header package must be read
-        first.  Assuming the stream palette hasn't loaded yet, this will call the Assembler object to check and see if
-        the stream header, along with its ASCII component have been fully read yet.  If so, it is loaded into the proper
-        class attributes.
-        '''
-
-        logging.debug('Attempting stream palette load...')
-        save_object = self.config_object.assembler.save_dict[self.stream_sha_from_last_frame].return_stream_header_id()
-
-        if save_object[0] == True:
-
-            self.stream_header_cleared = True
-
-            # Does stream palette already exist as a custom or default color?
-            if save_object[1] in self.config_object.color_handler.custom_palette_list or save_object[1] in \
-                    self.config_object.color_handler.default_palette_list:
-
-                self.stream_palette = palette_grabber(save_object[1])
-                logging.info(f'Palette ID {save_object[1]} already saved in system... successfully loaded!')
-
-            # This is a new palette which will now be instantiation as a custom palette object.
-            else:
-
-                self.fatal_checkpoint =_validate_and_add_palette(save_object[2], save_object[3], save_object[4], save_object[5])
-
-                if self.fatal_checkpoint == False:
-                    logging.critical('Palette for this stream cannot be loaded!  This could only be caused by data '
-                                     'corrupted during the streams write.  Aborting...')
-                    return False
-
-                logging.debug('Custom palette successfully instantiated.')
-                self.stream_palette = palette_grabber(save_object[1])
-
-            self.stream_palette_dict = ColorsToValue(self.stream_palette)
-            self.frame_handler.update_dictionaries('stream_palette', self.stream_palette_dict, self.stream_palette)
-            self.config_object.assembler.save_dict[self.stream_sha].stream_palette_read = True
-
-        else:
-            logging.debug('Attempt failed this frame.')
-
-
-    def _image_frame_setup(self):
-        '''This method validates the initializer bit string, as well as loads the primary palette into memory.  The
-        reason why this must be a separate method from _video_first_frame_setup is because both images and videos handle
-        palettes differently.  Images only need to worry about the "primary palette" or the palette that is being used
-        on that particular frame, while videos must intelligently switch between the header palette and the stream
-        palette.
-        '''
-
-        self.protocol_version, self.primary_palette = read_initializer(self.frame_handler.return_initializer(),
-                                                                       self.block_height, self.block_width,
-                                                                       self.config_object.color_handler.custom_palette_list,
-                                                                       self.config_object.color_handler.default_palette_list)
-        if self.protocol_version == False:
-            return False
-        logging.debug(f'primary_palette ID loaded: {self.primary_palette.id}')
-
-        self.primary_palette_dict = ColorsToValue(self.primary_palette)
-        self.frame_handler.primary_palette_bit_length = self.primary_palette.bit_length
-        self.frame_handler.update_dictionaries('primary_palette', self.primary_palette_dict, self.primary_palette)
-
-
-    def _video_first_frame_setup(self):
-        '''This method is ran on the first frame of the decoded video, to load in the header palette.  To prevent a
-        duplicate explanation, please see _imagePaletteSetup directly above.
-        '''
-
-        self.protocol_version, self.header_palette = read_initializer(self.frame_handler.return_initializer(),
-                                                                      self.block_height, self.block_width,
-                                                                      self.config_object.color_handler.custom_palette_list,
-                                                                      self.config_object.color_handler.default_palette_list)
-        if self.protocol_version == False:
-            return False
-        logging.debug(f'header_palette ID loaded: {self.header_palette.id}')
-
-        # Now with header_palette loaded, we can get its color set as well as generate its ColorsToValue dictionary,
-        # As well as propagate these values to frame_handler.
-        self.header_palette_dict = ColorsToValue(self.header_palette)
-        self.frame_handler.update_dictionaries('header_palette', self.header_palette_dict, self.header_palette)
\ No newline at end of file
diff --git a/bitglitter/read/decoderassets.py b/bitglitter/read/decoderassets.py
deleted file mode 100644
index 75e2be6..0000000
--- a/bitglitter/read/decoderassets.py
+++ /dev/null
@@ -1,162 +0,0 @@
-import hashlib
-import logging
-import numpy
-import zlib
-
-from bitglitter.palettes.paletteutilities import palette_grabber
-from bitglitter.protocols.protocolhandler import protocol_handler
-
-
-def minimum_block_checkpoint(block_height_override, block_width_override, active_frame_size_width,
-                             active_frame_size_height):
-    '''If block_height_override and block_width_override have been entered, this checks those values against the height
-    and width (in pixels) of the image loaded).  Since the smallest blocks that can be read are one pixels (since that
-    is finest detail you can have with an image), any values beyond that are invalid, and stopped here.
-    '''
-
-    if block_height_override and block_width_override:
-        if active_frame_size_width < block_width_override or active_frame_size_height < \
-                block_height_override:
-            logging.warning("Block override parameters are too large for a file of these dimensions.  "
-                            "Aborting...")
-            return False
-
-    return True
-
-
-def scan_block(image, pixel_width, block_width_position, block_height_position):
-    '''This function is what's used to scan the blocks used.  First the scan area is determined, and then each of the
-    pixels in that area appended to a list.  An average of those values as type int is returned.
-    '''
-
-    if pixel_width < 5:
-        start_position_x = int(block_width_position * pixel_width)
-        end_position_x = int((block_width_position * pixel_width) + pixel_width - 1)
-        start_position_y = int(block_height_position * pixel_width)
-        end_position_y = int((block_height_position * pixel_width) + pixel_width - 1)
-
-    else:
-        start_position_x = int(round((block_width_position * pixel_width) + (pixel_width * .25), 1))
-        end_position_x = int(round(start_position_x + (pixel_width * .5), 1))
-        start_position_y = int(round((block_height_position * pixel_width) + (pixel_width * .25), 1))
-        end_position_y = int(round(start_position_y + (pixel_width * .5), 1))
-
-    numpy_output = numpy.flip(image[start_position_y:end_position_y, start_position_x:end_position_x]).mean(axis=(0,1))
-    to_list_format = numpy_output.tolist()
-
-    for value in range(3):
-        to_list_format[value] = int(to_list_format[value])
-
-    return to_list_format
-
-
-def read_initializer(bit_stream, blockHeight, block_width, custom_palette_list, default_palette_list):
-    '''This function decodes the raw binary data from the initializer header after verifying it's checksum, and will
-    emergency stop the read if any of the conditions are met:  If the read checksum differs from the calculated
-    checksum, if the read protocol version isn't supported by this BitGlitter version, if the read_block_height or
-    read_block_width differ from what frame_lock_on() read, or if the palette ID for the header is unknown (ie, a custom
-    color which has not been integrated yet).  Returns protocol_version and header_palette object.
-    '''
-
-    # First, we're verifying the initializer is not corrupted by comparing its read checksum with a calculated one from
-    # it's contents.  If they match, we continue.  If not, this frame aborts.
-    logging.debug('read_initializer running...')
-
-    bit_stream.pos = 0
-    full_bit_stream_to_hash = bit_stream.read('bits : 292')
-    converted_to_bytes = full_bit_stream_to_hash.tobytes()
-    calculated_crc = zlib.crc32(converted_to_bytes)
-    read_crc = bit_stream.read('uint : 32')
-    if calculated_crc != read_crc:
-        logging.warning('Initializer checksum failure.  Aborting...')
-        return False, False
-
-    bit_stream.pos = 0
-    protocol_version = bit_stream.read('uint : 4')
-    logging.debug(f'Protocol version: {protocol_version}')
-    if str(protocol_version) not in protocol_handler.available_protocols:
-        logging.warning(f'Protocol v{str(protocol_version)} not supported in this version of BitGlitter.  Please update'
-                        f' to fix.  Aborting...')
-        return False, False
-
-    read_block_height = bit_stream.read('uint : 16')
-    read_block_width = bit_stream.read('uint : 16')
-    if read_block_height != blockHeight or read_block_width != block_width:
-        logging.warning('read_initializer: Geometry assertion failure.  Aborting...')
-        logging.debug(f'read_block_height: {read_block_height}\n blockHeight {blockHeight}'
-                      f'\n read_block_width {read_block_width}\n block_width {block_width}')
-
-        return False, False
-
-    bit_stream.pos += 232
-    frame_palette_id = bit_stream.read('uint : 24')
-
-    if frame_palette_id > 100:
-
-        bit_stream.pos -= 256
-        frame_palette_id = bit_stream.read('hex : 256')
-        frame_palette_id.lower()
-
-        if frame_palette_id not in custom_palette_list:
-
-            logging.warning('read_initializer: This header palette is unknown, reader cannot proceed until it is '
-                            'learned through a \nstream header.  This can occur if the creator of the stream uses a '
-                            'non-default palette.  This can also trigger if frames \nare read non-sequentially.  '
-                            'Aborting...')
-            return False, False
-
-    else:
-
-        if str(frame_palette_id) not in default_palette_list:
-            logging.warning('read_initializer: This default palette is unknown by this version of BitGlitter.  This\n'
-                            "could be the case if you're using an older version.  Aborting...")
-            logging.debug(f'frame_palette_id: {frame_palette_id}\ndefault_palette_list: {default_palette_list}')
-            return False, False
-
-    frame_palette = palette_grabber(str(frame_palette_id))
-    logging.debug('read_initializer successfully ran.')
-    return protocol_version, frame_palette
-
-
-def read_frame_header(bit_stream):
-    '''While read_initializer is mostly used for verification of values, this function's purpose is to return values
-    needed for the reading process, once verified.  Returns stream_sha, frame_sha, frame_number, and blocks_to_read.
-    '''
-
-    logging.debug('read_frame_header running...')
-    full_bit_stream_to_hash = bit_stream.read('bytes : 72')
-
-    calculated_crc = zlib.crc32(full_bit_stream_to_hash)
-    read_crc = bit_stream.read('uint : 32')
-    if calculated_crc != read_crc:
-        logging.warning('frameHeader checksum failure.  Aborting...')
-        return False, False, False, False
-
-    bit_stream.pos = 0
-    stream_sha = bit_stream.read('hex : 256')
-    frame_sha = bit_stream.read('hex : 256')
-    frame_number = bit_stream.read('uint : 32')
-    blocks_to_read = bit_stream.read('uint : 32')
-
-    logging.debug('read_frame_header successfully ran.')
-    return stream_sha, frame_sha, frame_number, blocks_to_read
-
-
-def validate_payload(payload_bits, read_frame_sha):
-    '''Taking all of the frame bits after the frame header, this takes the SHA-256 hash of them, and compares it against
-    the frame SHA written in the frame header.  This is the primary mechanism that validates frame data, which either
-    allows it to be passed through to the assembler, or discarded.
-    '''
-
-    sha_hasher = hashlib.sha256()
-    sha_hasher.update(payload_bits.tobytes())
-    string_output = sha_hasher.hexdigest()
-    logging.debug(f'length of payload_bits: {payload_bits.len}')
-
-    if string_output != read_frame_sha:
-        logging.warning('validate_payload: read_frame_sha does not match calculated one.  Aborting...')
-        logging.debug(f'Read from frameHeader: {read_frame_sha}\nCalculated just now: {string_output}')
-        return False
-
-    logging.debug('Payload validated this frame.')
-    return True
\ No newline at end of file
diff --git a/bitglitter/read/fileslicer.py b/bitglitter/read/fileslicer.py
deleted file mode 100644
index 8de2bf4..0000000
--- a/bitglitter/read/fileslicer.py
+++ /dev/null
@@ -1,80 +0,0 @@
-import logging
-import os
-
-from cv2 import imread
-
-from bitglitter.config.constants import VALID_VIDEO_FORMATS
-from bitglitter.read.decoder import Decoder
-from bitglitter.read.videoframepuller import VideoFramePuller
-
-
-def file_slicer(file_to_input, active_path, output_path, block_height_override, block_width_override,
-                encryption_key, scrypt_n, scrypt_r, scrypt_p, config_object, bad_frame_strikes, assemble_hold):
-    '''This function is responsible for handling the 'physical' frames of the stream, and feeding them into the Decoder
-    object.  It also checks to see if various checkpoints are passed, and will abort out of read if critical errors in
-    the stream are found.
-    '''
-
-    # Sets up workspace
-    read_root = os.path.join(os.getcwd(), active_path)
-    working_folder = read_root + '\\Working Folder'
-    if not os.path.isdir(read_root):
-        os.makedirs(read_root)
-    config_object.assembler.workingFolder = read_root
-
-    # Determines if the multimedia file is an image or a video.
-    is_video = False
-    if os.path.splitext(file_to_input)[1] in VALID_VIDEO_FORMATS:
-        is_video = True
-
-    decoder = Decoder(is_video, config_object, scrypt_n, scrypt_r, scrypt_p, block_height_override,
-                      block_width_override, output_path, encryption_key, assemble_hold)
-
-    if is_video:
-
-        # If there is a serious frame failure, this will increase by one.  If the read is successful, it resets back to
-        # zero.  More than five in a row will abort the entire read.  This is to protect against the reader spending a
-        # lot of time attempting to read long videos with one failure after the next.
-        bad_frames_this_session = 0
-
-        video_frame_puller = VideoFramePuller(file_to_input)
-
-        for frame in range(video_frame_puller.total_frames):
-
-            if decoder.fatal_checkpoint == False:
-                break
-
-            if bad_frames_this_session < bad_frame_strikes or bad_frame_strikes == 0:
-                logging.info(f'Processing frame {video_frame_puller.current_frame} of '
-                             f'{video_frame_puller.total_frames}...')
-                active_frame = video_frame_puller.next_frame()
-
-                if decoder.decode_video_frame(active_frame) == False:
-                    if decoder.duplicate_frame_read == False:
-                        bad_frames_this_session += 1
-
-                        if bad_frame_strikes > 0:
-                            logging.warning(f'Bad frame strike {bad_frames_this_session} / {bad_frame_strikes}')
-
-                        else:
-                            logging.warning(f'Bad frame strike # {bad_frames_this_session}')
-
-                config_object.save_session()
-
-            else:
-                logging.info(f'Breaking out of video after reaching {bad_frame_strikes} bad frame strikes...')
-                break
-
-        return True
-
-    else:
-        logging.info("Processing image...")
-        checkpointPassed = decoder.decode_image(imread(file_to_input))
-
-        if checkpointPassed == False:
-            logging.debug('Breaking out of file_slicer(), checkpoint_passed == False')
-            return False
-
-        config_object.save_session()
-        logging.info("Image successfully processed.")
-        return True
\ No newline at end of file
diff --git a/bitglitter/read/framehandler.py b/bitglitter/read/framehandler.py
deleted file mode 100644
index 50dd215..0000000
--- a/bitglitter/read/framehandler.py
+++ /dev/null
@@ -1,190 +0,0 @@
-import logging
-import math
-
-from bitstring import BitStream
-
-from bitglitter.read.coloranalysis import color_snap
-from bitglitter.read.decoderassets import scan_block
-from bitglitter.config.config import config
-
-
-class FrameHandler:
-    '''FrameHandler object is what traverses the actual frame, and returns bit data.  It's designed as an easy to use
-    API, hiding much of the moving parts behind the scenes of a frame scan.
-    '''
-
-    def __init__(self, initializer_palette, initializer_palette_dict):
-
-        # Setup
-        self.image = None
-        self.block_height = None
-        self.block_width = None
-        self.pixel_width = None
-
-        # Palette Variables
-        self.initializer_palette = initializer_palette
-        self.initializer_palette_dict = initializer_palette_dict
-        self.header_palette = None
-        self.header_palette_dict = None
-        self.stream_palette = None
-        self.stream_palette_dict = None
-        self.primary_palette = None
-        self.primary_palette_dict = None
-
-        # Dictionaries
-        self.palette_dict = {'initializer_palette': self.initializer_palette,
-                            'primary_palette': self.primary_palette,
-                            'header_palette': self.header_palette,
-                            'stream_palette': self.stream_palette}
-        self.palette_conversion_dict = {'initializer_palette' : self.initializer_palette_dict,
-                                      'primary_palette': self.primary_palette_dict,
-                                      'header_palette' : self.header_palette_dict,
-                                      'stream_palette' : self.stream_palette_dict}
-
-        # Scan State
-        self.is_first_frame = True
-        self.non_calibrator_blocks = 0
-        self.next_block = 0
-        self.block_position = 0
-
-
-    def _setup_frame_grid(self, has_initializer):
-        '''Ran on initialization, this creates a generator that outputs the correct block coordinates for scan_block
-        to utilize.  This depends on whether an initializer is used in this frame or not.
-        '''
-
-        for y_block in range(self.block_height - int(has_initializer)):
-            for x_block in range(self.block_width - int(has_initializer)):
-                yield x_block + int(has_initializer), y_block + int(has_initializer)
-
-
-    def _blocks_to_bits(self, how_many, palette_type):
-        '''This is an internal method that, based on how many blocks it was told to scan as well as the palette type
-        used, will scan that amount on the image and return those converted bits.
-        '''
-
-        bit_string = BitStream()
-        active_color_set = self.palette_dict[palette_type].color_set
-        active_palette_dict = self.palette_conversion_dict[palette_type]
-
-        for block in range(how_many):
-            block_coords = next(self.next_block)
-            raw_rgb = scan_block(self.image, self.pixel_width, block_coords[0], block_coords[1])
-            if active_color_set:
-                bit_string.append(active_palette_dict.get_value(color_snap(raw_rgb, active_color_set)))
-
-            else:
-                bit_string.append(active_palette_dict.get_value(raw_rgb))
-
-        self.block_position += how_many
-        config.stats_handler.blocks_read += how_many
-
-        return bit_string
-
-
-    def return_initializer(self):
-        '''This returns the initializer bitstring for the frame.'''
-
-        return self._blocks_to_bits(324, 'initializer_palette')
-
-
-    def return_frame_header(self, palette_type):
-        '''This method returns the bits carrying the frame header for the frame, as well as the "carry over" bits, which
-        were the excess capacity within those blocks, assuming there was extra space.
-        '''
-
-        # always 608 bits, plus whatever remainder bits that may be present in the final block.  Protocol v1 only!
-        if palette_type != 'stream_palette' and palette_type != 'header_palette' and palette_type != 'primary_palette':
-            raise ValueError("FrameHandler.return_frame_header: invalid palette_type argument.")
-
-        full_block_data = self._blocks_to_bits(math.ceil(608 / self.palette_dict[palette_type].bit_length),
-                                               f'{palette_type}')
-        carry_over_bits = BitStream()
-
-        if full_block_data.len > 608:
-
-            full_block_data.pos = 608
-            carry_over_bits.append(full_block_data.read(f'bits : {full_block_data.len - 608}'))
-            full_block_data.pos = 0
-
-        config.stats_handler.data_read += carry_over_bits.len
-        return full_block_data.read('bits : 608'), carry_over_bits
-
-
-    def return_remainder_payload(self, palette_type):
-        '''With the other parts of the frame already scanned (initializer if applicable, and frame header), based on how
-        many blocks are left, it will scan those and return the bit string.
-        '''
-
-
-        if palette_type != 'stream_palette' and palette_type != 'header_palette' and palette_type != 'primary_palette':
-            raise ValueError("FrameHandler.return_remainder_payload: invalid palette_type argument.")
-
-        remainder_payload = self._blocks_to_bits(self.non_calibrator_blocks - self.block_position, f'{palette_type}')
-        config.stats_handler.data_read += remainder_payload.len
-
-        return remainder_payload
-
-
-    def update_scan_geometry(self, block_height, block_width, pixel_width):
-        '''FrameHandler is a persistent object between frames, held by Decoder.  Because of it's instantiation at
-        Decoder's instantiation, scan geometry cannot be loaded into it immediately.  This function does that.
-        '''
-
-        self.block_height = block_height
-        self.block_width = block_width
-        self.pixel_width = pixel_width
-
-        self.non_calibrator_blocks = (self.block_height - int(self.is_first_frame)) * \
-                                     (self.block_width - int(self.is_first_frame))
-
-
-    def update_dictionaries(self, palette_type, palette_dict, palette):
-        '''As the stream is setting up, its not possible to instantiate this object with everything at once.  This
-        method allows for an easy way to inject new palette objects into the dictionaries used for retrieving values.
-        '''
-
-        if palette_type == 'header_palette':
-            self.palette_conversion_dict['header_palette'] = palette_dict
-            self.palette_dict['header_palette'] = palette
-        elif palette_type == 'stream_palette':
-            self.palette_conversion_dict['stream_palette'] = palette_dict
-            self.palette_dict['stream_palette'] = palette
-        elif palette_type == 'primary_palette':
-            self.palette_conversion_dict['primary_palette'] = palette_dict
-            self.palette_dict['primary_palette'] = palette
-        else:
-            raise ValueError('FrameHandler.updateDictionary: invalid palette_type argument.')
-
-
-    def update_blocks_to_read(self, blocks_to_read):
-        '''After the frame header bit string is successfully read by read_frame_header(), this updates how many total
-        blocks must be read on this frame.  Until that is known, FrameHandler will simply 'blindly' read the full frame,
-        as it is instructed to in pieces.
-        '''
-
-
-        if blocks_to_read < self.non_calibrator_blocks:
-            logging.debug(f'Last frame detected, {blocks_to_read} to scan.')
-        else:
-            logging.debug('Full frame detected.')
-
-        self.non_calibrator_blocks = blocks_to_read
-
-
-    def load_new_frame(self, image, has_initializer):
-        '''This method loads in the new image, as well as resets the parameters from the previous frame.'''
-
-        self.image = image
-        self.is_first_frame = has_initializer
-        self.next_block = self._setup_frame_grid(has_initializer)
-        self.block_position = 0
-
-        # This will only fail once, as geometry is not immediately known on the loading of the first frame.
-        try:
-            self.non_calibrator_blocks = (self.block_height - int(has_initializer)) * \
-                                         (self.block_width - int(has_initializer))
-        except:
-            pass
-
-        config.stats_handler.frames_read += 1
\ No newline at end of file
diff --git a/bitglitter/read/framelockon.py b/bitglitter/read/framelockon.py
deleted file mode 100644
index eddc0d7..0000000
--- a/bitglitter/read/framelockon.py
+++ /dev/null
@@ -1,229 +0,0 @@
-import logging
-
-from bitstring import BitArray, ConstBitStream
-from numpy import flip
-
-from bitglitter.read.coloranalysis import color_snap, return_distance
-from bitglitter.read.decoderassets import scan_block
-from bitglitter.palettes.paletteutilities import palette_grabber, ColorsToValue
-
-
-def frame_lock_on(image, block_height_override, block_width_override, frame_width, frame_height):
-    '''This function is used to lock onto the frame.  If override values are present, these will be used.  Otherwise,
-    it will attempt to extract the correct values from the X and Y calibrator on the initial frame.'''
-
-    logging.debug('Locking onto frame...')
-    initializer_palette_a = palette_grabber('1')
-    initializer_palette_b = palette_grabber('11')
-    initializer_palette_a_dict = ColorsToValue(initializer_palette_a)
-    initializer_palette_b_dict = ColorsToValue(initializer_palette_b)
-    combined_colors = initializer_palette_a.color_set + initializer_palette_b.color_set
-
-    if block_height_override and block_width_override: # Jump straight to verification
-        logging.info("block_height_override and block_width_override parameters detected.  Attempting to lock with "
-                     "these values...")
-        pixel_width = ((frame_width / block_width_override) + (frame_height / block_height_override)) / 2
-
-        checkpoint = verify_blocks_x(image, pixel_width, block_width_override, combined_colors,
-                                     initializer_palette_a_dict, initializer_palette_b_dict, override=True)
-        if checkpoint == False:
-                return False, False, False
-
-        checkpoint = verify_blocks_y(image, pixel_width, block_height_override, combined_colors,
-                                     initializer_palette_a_dict, initializer_palette_b_dict, override=True)
-        if checkpoint == False:
-                return False, False, False
-
-        block_width, block_height = block_width_override, block_height_override
-
-    else:
-        # First checkpoint.  Does pixel 0,0 have color_distance value of under 100 for black (0,0,0)?
-        if return_distance(image[0, 0], (0, 0, 0)) > 100:
-            logging.warning('Frame lock fail!  Initial pixel value exceeds maximum color distance allowed for a '
-                            'reliable lock.')
-            return False, False, False
-
-        pixel_width, block_dimension_guess = pixel_creep(image, initializer_palette_a, initializer_palette_b,
-                                                         combined_colors, initializer_palette_a_dict,
-                                                         initializer_palette_b_dict, frame_width, frame_height,
-                                                         width=True)
-        checkpoint = verify_blocks_x(image, pixel_width, block_dimension_guess, combined_colors,
-                                     initializer_palette_a_dict, initializer_palette_b_dict)
-        if checkpoint == False:
-            return False, False, False
-
-        block_width = block_dimension_guess
-        pixel_width, block_dimension_guess = pixel_creep(image, initializer_palette_a, initializer_palette_b,
-                                                         combined_colors, initializer_palette_a_dict,
-                                                         initializer_palette_b_dict, frame_width, frame_height,
-                                                         width=False)
-        checkpoint = verify_blocks_y(image, pixel_width, block_dimension_guess, combined_colors,
-                                     initializer_palette_a_dict, initializer_palette_b_dict)
-
-        if checkpoint == False:
-            return False, False, False
-        block_height = block_dimension_guess
-
-    logging.debug(f'Lockon successful.\npixel_width: {pixel_width}\nblock_height: {block_height}\nblock_width: '
-                  f'{block_width}')
-    return block_height, block_width, pixel_width
-
-
-def verify_blocks_x(image, pixel_width, block_width_estimate, combined_colors, initializer_palette_a_dict,
-                    initializer_palette_b_dict, override = False):
-    '''This is a function used within frame_lock_on().  It verifies the correct values for the X axis.'''
-
-    calibrator_bits_x = BitArray()
-    for x_block in range(17):
-        snapped_value = color_snap(scan_block(image, pixel_width, x_block, 0), combined_colors)
-
-        if x_block % 2 == 0:
-            calibrator_bits_x.append(initializer_palette_a_dict.get_value(snapped_value))
-
-        else:
-            calibrator_bits_x.append(initializer_palette_b_dict.get_value(snapped_value))
-
-    calibrator_bits_x.reverse()
-    read_calibrator_x = ConstBitStream(calibrator_bits_x)
-
-    if read_calibrator_x.read('uint:16') != block_width_estimate:
-        if override == True:
-            logging.warning('block_width_override is not equal to what was read on calibrator.  Aborting...')
-
-        else:
-            logging.warning('block_width verification does not match initial read.  This could be the result of \n'
-                            'sufficiently distorted frames.  Aborting...')
-
-        return False
-
-    if read_calibrator_x.read('bool') != False:
-        logging.warning('0,0 block unexpected value.  Aborting...')
-        return False
-
-    if override == True:
-        logging.info('block_width_override successfully verified.')
-
-    else:
-        logging.debug('block_width successfully verified.')
-
-    return True
-
-
-def verify_blocks_y(image, pixel_width, block_height_estimate, combined_colors, initializer_palette_a_dict,
-                    initializer_palette_b_dict, override = False):
-    '''This is a function used within frame_lock_on().  It verifies the correct values for the Y axis.'''
-
-    calibrator_bits_y = BitArray()
-    for y_block in range(17):
-        snapped_value = color_snap(scan_block(image, pixel_width, 0, y_block), combined_colors)
-
-        if y_block % 2 == 0:
-            calibrator_bits_y.append(initializer_palette_a_dict.get_value(snapped_value))
-
-        else:
-            calibrator_bits_y.append(initializer_palette_b_dict.get_value(snapped_value))
-
-    calibrator_bits_y.reverse()
-    read_calibrator_y = ConstBitStream(calibrator_bits_y)
-
-    if read_calibrator_y.read('uint:16') != block_height_estimate:
-        if override == True:
-            logging.warning('block_height_override is not equal to what was read on calibrator.  Aborting...')
-
-        else:
-            logging.warning('block_height verification does not match initial read.  This could be the result of \n'
-                            'sufficiently distorted frames.  Aborting...')
-
-        return False
-
-    if read_calibrator_y.read('bool') != False:
-        logging.warning('0,0 block unexpected value.  Aborting...')
-        return False
-
-    if override == True:
-        logging.info('block_height_override successfully verified.')
-
-    else:
-        logging.debug('block_height successfully verified.')
-
-    return True
-
-
-def pixel_creep(image, initializer_palette_a, initializer_palette_b, combined_colors, initializer_palette_a_dict,
-                initializer_palette_b_dict, image_width, image_height, width):
-    '''This function moves across the calibrator on the top and left of the frame one pixel at a time, and after
-    'snapping' the colors, decodes an unsigned integer from each axis, which if read correctly, is the block width and
-    block height of the frame.
-    '''
-
-    calibrator_bits = BitArray()
-    snapped_values = []
-    active_color = (0, 0, 0)
-    active_distance = 0
-    pixel_on_dimension = 1
-    palette_a_is_active = False
-
-    if width == True:
-        axis_on_image = pixel_on_dimension, 0
-        axis_analyzed = image_width
-
-    else:
-        axis_on_image = 0, pixel_on_dimension
-        axis_analyzed = image_height
-
-    for value in range(16):
-        while True:
-            if width == True:
-                axis_on_image = 0, pixel_on_dimension
-                axis_analyzed = image_width
-
-            else:
-                axis_on_image = pixel_on_dimension, 0
-                axis_analyzed = image_height
-
-            new_palette_locked = False
-            active_scan = flip(image[axis_on_image])
-            active_distance = return_distance(active_scan, active_color)
-
-            pixel_on_dimension += 1
-            if active_distance < 100:  # Iterating over same colored blocks, until distance exceeds 100.
-                continue
-
-            else:  # We are determining if we are within < 100 dist of a new color, or are in fuzzy space.
-                if palette_a_is_active == False:
-                    active_palette = initializer_palette_b.color_set
-
-                else:
-                    active_palette = initializer_palette_a.color_set
-
-                for color in active_palette:
-                    active_distance = return_distance(active_scan, color)
-
-                    if active_distance < 100:
-                        palette_a_is_active = not palette_a_is_active
-                        new_palette_locked = True
-                        break
-
-                    else:
-                        continue
-
-            if new_palette_locked == True:
-                break
-
-        active_color = color_snap(active_scan, combined_colors)
-        snapped_values.append(active_color)
-
-        if value % 2 != 0:
-            calibrator_bits.append(initializer_palette_a_dict.get_value(active_color))
-
-        else:
-            calibrator_bits.append(initializer_palette_b_dict.get_value(active_color))
-
-        active_distance = 0
-
-    calibrator_bits.reverse()
-    read_calibrator_bits = ConstBitStream(calibrator_bits)
-    block_dimension_guess = read_calibrator_bits.read('uint:16')
-    pixel_width = axis_analyzed / block_dimension_guess
-
-    return pixel_width, block_dimension_guess
\ No newline at end of file
diff --git a/bitglitter/read/partialsave.py b/bitglitter/read/partialsave.py
deleted file mode 100644
index 702c0ba..0000000
--- a/bitglitter/read/partialsave.py
+++ /dev/null
@@ -1,461 +0,0 @@
-import datetime
-import logging
-import os
-
-from bitstring import BitStream
-
-from bitglitter.protocols.protocol_one.read.protocol_one_postprocess import PostProcessor
-from bitglitter.read.partialsaveassets import format_file_list, decode_stream_header_ascii_compressed, \
-    decode_stream_header_binary_preamble
-from bitglitter.utilities.filemanipulation import compress_file, decompress_file, return_hash_from_file
-
-
-class PartialSave:
-    '''PartialSave objects are essentially containers for the state of streams as they are being read, frame by frame.
-    This mainly interacts through the Assembler object.  All of the functionality needed to convert raw frame data back
-    into the original package is done through it's contained methods.
-    '''
-
-    def __init__(self, stream_sha, working_folder, scrypt_n, scrypt_r, scrypt_p, output_path, encryption_key,
-                 assemble_hold):
-
-        # Core object state data
-        self.save_folder = working_folder + f'\\{stream_sha}'
-        self.stream_sha = stream_sha
-        self.assembled_sha = stream_sha
-        self.frames_ingested = 0
-
-        self.stream_header_preamble_complete = False
-        self.stream_header_ascii_complete = False
-        self.stream_header_preamble_buffer = BitStream()
-        self.stream_header_ascii_buffer = BitStream()
-
-        self.next_stream_header_sequential_frame = 1
-        self.payload_begins_this_frame = None
-        self.active_this_session = True
-        self.is_assembled = False # Did the frames successfully merge into a single binary?
-        self.post_processor_decrypted = False # Was the stream successfully decrypted?
-        self.frame_reference_table = None
-        self.frames_prior_to_binary_preamble = []
-        self.stream_palette_read = False
-        self.assemble_hold = assemble_hold
-
-
-        # Stream Header - Binary Preamble
-        self.size_in_bytes = None
-        self.total_frames = '???'
-        self.compression_enabled = None
-        self.encryption_enabled = None
-        self.masking_enabled = None
-        self.custom_palette_used = None
-        self.date_created = None
-        self.stream_palette_id = None
-        self.ascii_header_byte_size = None
-
-        # Stream Metadata
-        self.bg_version = None
-        self.stream_name = None
-        self.stream_description = None
-        self.file_list = None
-
-        # Optional ASCII Header Fields
-        self.custom_color_name = None
-        self.custom_color_description = None
-        self.custom_color_date_created = None
-        self.custom_color_palette = None
-        self.post_compression_sha = None
-
-        # Changeable Postprocessing Arguments
-        self.encryption_key = encryption_key
-        self.scrypt_n = scrypt_n
-        self.scrypt_r = scrypt_r
-        self.scrypt_p = scrypt_p
-        self.output_path = output_path
-
-        os.mkdir(self.save_folder)
-        logging.info(f'New partial save! Stream SHA-256: {self.stream_sha}')
-
-
-    def load_frame_data(self, frame_data, frame_number, is_recursive = False):
-        '''After being validated in the decoder, this method blindly accepts the frame_data as a piece, saving it within
-        the appropriate folder, and adding the frame number to the list.
-        '''
-
-        # This makes the count increase only when this function isn't ran recursively.  This prevents revisited frames
-        # from increasing the count.
-        if is_recursive == False:
-            self.frames_ingested += 1
-
-        if self.stream_header_ascii_complete == False and frame_number == self.next_stream_header_sequential_frame:
-            frame_data = self._stream_header_assembly(frame_data, frame_number)
-
-        if frame_data.len > 0:
-            self._write_file(frame_data, f'frame{frame_number}')
-
-        self.active_this_session = True
-
-        if self.stream_header_preamble_complete == True:
-            self.frame_reference_table.set(True, frame_number - 1)
-
-        else:
-            self.frames_prior_to_binary_preamble.append(frame_number)
-
-        logging.debug(f"Frame {frame_number} for stream {self.stream_sha} successfully saved!")
-
-        if self.stream_header_ascii_complete == False \
-            and self.is_frame_needed(self.next_stream_header_sequential_frame) == False:
-            frame_data = self._read_file(f'frame{self.next_stream_header_sequential_frame}')
-            self.load_frame_data(frame_data, self.next_stream_header_sequential_frame, is_recursive= True)
-
-
-    def user_input_update(self, password_update, scrypt_n, scrypt_r, scrypt_p, change_output_path):
-        '''This method changes user related configurations such as password, scrypt parameters, and save location.
-        These arguments are blindly accepted from update_partial_save() in savedfilefunctions, as the inputs are
-        validated there.
-        '''
-
-        if password_update:
-            self.encryption_key = password_update
-
-        if scrypt_n:
-            self.scrypt_n = scrypt_n
-        if scrypt_r:
-            self.scrypt_r = scrypt_r
-        if scrypt_p:
-            self.scrypt_p = scrypt_p
-
-        if change_output_path:
-            self.output_path = change_output_path
-
-
-    def _attempt_assembly(self):
-        '''This method will check to see if all frames for the stream have been read.  If so, they will be assembled
-        into a single binary, it's hash will be validated, and then it will be ran through post-processing,
-        which is what ultimately yields the original files encoded in the stream.
-        '''
-
-        if self.is_assembled == False: # If false, assembly will be attempted.  Otherwise, we skip to postprocessing.
-
-            if self.total_frames == self.frames_ingested:
-                logging.info(f'All frame(s) loaded for {self.stream_sha}, attempting assembly...')
-                data_left = self.size_in_bytes * 8
-                assembled_path = f'{self.save_folder}\\assembled.bin'
-                inter_frame_bit_buffer = BitStream()
-
-                with open(assembled_path, 'ab') as assemble_package:
-
-                    for frame in range(self.total_frames - self.payload_begins_this_frame + 1):
-
-                        frame_number = frame + self.payload_begins_this_frame
-                        logging.debug(f'Assembling {frame_number}/{self.total_frames}')
-                        active_frame = self._read_file(f'frame{frame_number}')
-
-                        if frame_number != self.total_frames: # All frames except the last one.
-                            bit_merge = BitStream(inter_frame_bit_buffer + active_frame)
-                            data_holder = bit_merge.read(f'bytes: {bit_merge.len // 8}')
-
-                            if bit_merge.len - bit_merge.pos > 0:
-                                inter_frame_bit_buffer = bit_merge.read(f'bits : {bit_merge.len - bit_merge.pos}')
-
-                            else:
-                                inter_frame_bit_buffer = BitStream()
-
-                            if isinstance(data_holder, bytes):
-                                logging.debug('was bytes this frame!')
-                                assemble_package.write(data_holder)
-
-                            else:
-                                logging.debug('bits to bytes this frame')
-                                to_byte_type = data_holder.tobytes()
-                                assemble_package.write(to_byte_type)
-                            data_left -= active_frame.len
-
-                        else: #This is the last frame
-                            bit_merge = inter_frame_bit_buffer + active_frame.read(f'bits : {data_left}')
-                            to_byte_type = bit_merge.tobytes()
-                            assemble_package.write(to_byte_type)
-
-                if return_hash_from_file(assembled_path) != self.assembled_sha:
-                    logging.critical(f'Assembled frames do not match self.packageSHA.  Cannot continue.')
-                    return False
-
-                logging.debug(f'Successfully assembled.')
-                self.is_assembled = True
-
-            else:
-                logging.info(f'{self.frames_ingested} / {self.total_frames}  frames have been loaded for '
-                             f'{self.stream_sha}\n so far, cannot assemble yet.')
-                return False
-
-        post_process_attempt = PostProcessor(self.output_path, self.stream_sha, self.save_folder,
-                                           self.encryption_enabled, self.encryption_key, self.scrypt_n,
-                                           self.scrypt_r, self.scrypt_p, self.compression_enabled)
-
-        # Did all three stages of PostProcess successfully run?
-        if post_process_attempt.fully_assembled != True:
-            return False
-
-        return True
-
-
-    def _create_frame_reference_table(self):
-        '''This creates a file inside of the object's folder to keep track of which frames have been ingested.  Bit
-        positions correspond to frame number (-1 so position 0 is frame 1, etc).
-        '''
-
-        self.frame_reference_table = BitStream(length=self.total_frames)
-
-        for position in self.frames_prior_to_binary_preamble:
-            self.frame_reference_table.set(True, position - 1)
-
-        self.frames_prior_to_binary_preamble = []
-
-
-    def _stream_header_assembly(self, frame_data, frame_number):
-        '''Stream headers may span over numerous frames, and could be read non-sequentially.  This function manages this
-        aspect.  Both stream headers are stripped from the raw frame data, returning payload data (if applicable).
-        '''
-
-        logging.debug('_stream_header_assembly running...')
-        self.next_stream_header_sequential_frame += 1
-
-        if self.stream_header_preamble_complete == False:
-
-            # Preamble header uses all of this frame.
-            if 422 - self.stream_header_preamble_buffer.len >= frame_data.len:
-
-                logging.debug('Preamble header uses all of this frame.')
-                read_length = frame_data.len
-
-            # Preamble terminates on this frame.
-            else:
-
-                logging.debug('Preamble terminates on this frame.')
-                read_length = 422 - self.stream_header_preamble_buffer.len
-
-            self.stream_header_preamble_buffer.append(frame_data.read(f'bits:{read_length}'))
-
-            if self.stream_header_preamble_buffer.len == 422:
-                self.read_stream_header_binary_preamble()
-
-        if self.stream_header_ascii_complete == False and frame_data.len - frame_data.bitpos > 0:
-            # ASCII header rolls over to the next frame.
-            if self.ascii_header_byte_size * 8 - self.stream_header_ascii_buffer.len >= frame_data.len \
-                    - frame_data.bitpos:
-
-                logging.debug('ASCII header rolls over to another frame.')
-                read_length = frame_data.len - frame_data.bitpos
-
-            # ASCII header terminates on this frame.
-            else:
-                logging.debug('ASCII header terminates on this frame.')
-                read_length = self.ascii_header_byte_size * 8 - self.stream_header_ascii_buffer.len
-
-            self.stream_header_ascii_buffer.append(frame_data.read(f'bits:{read_length}'))
-
-            if self.stream_header_ascii_buffer.len == self.ascii_header_byte_size * 8:
-                self.payload_begins_this_frame = frame_number
-                self.read_stream_header_ascii_compressed()
-
-            if frame_data.len - frame_data.bitpos > 0:
-                return frame_data.read(f'bits:{frame_data.len - frame_data.bitpos}')
-
-        return BitStream()
-
-
-    def _write_file(self, data, file_name, to_compress=False):
-        '''This is an internal method used to write data to the object's folder.  Since we are dealing with bits and not
-        bytes (which is the smallest size operating systems can work with), there is a special five-byte header that
-        decodes as an unsigned integer which is the amount of bits to read.
-        '''
-
-        bits_appendage = BitStream(uint=data.len, length=40)
-        bits_appendage_to_bytes = bits_appendage.tobytes()
-        data_to_bytes = data.tobytes()
-
-        if to_compress == True:
-            temp_name = self.save_folder + '\\temp.bin'
-            with open(temp_name, 'wb') as write_data:
-                write_data.write(bits_appendage_to_bytes)
-                write_data.write(data_to_bytes)
-
-            compress_file(temp_name, self.save_folder + f'\\{file_name}.bin')
-
-        else:
-            with open(self.save_folder + f'\\{file_name}.bin', 'wb') as write_data:
-                write_data.write(bits_appendage_to_bytes)
-                write_data.write(data_to_bytes)
-
-
-    def _read_file(self, file_name, to_decompress=False):
-        '''This internal method reads the file with the file_name according to how many bits it is, deletes the file,
-        and then returns the BitStream object.
-        '''
-
-        file_path = self.save_folder + f'\\{file_name}.bin'
-
-        if to_decompress == False:
-            with open(file_path, 'rb') as readData:
-                file_to_bits = BitStream(readData)
-                bits_to_read = file_to_bits.read('uint:40')
-                retrieved_file = file_to_bits.read(f'bits:{bits_to_read}')
-            os.remove(file_path)
-
-        else:
-            decompress_file(file_path, self.save_folder + '\\temp.bin')
-            with open(self.save_folder + '\\temp.bin', 'rb') as readData:
-                file_to_bits = BitStream(readData)
-                bits_to_read = file_to_bits.read('uint:40')
-                retrieved_file = file_to_bits.read(f'bits:{bits_to_read}')
-            os.remove(self.save_folder + '\\temp.bin')
-
-        return retrieved_file
-
-
-    def read_stream_header_binary_preamble(self):
-        '''This method converts the raw full binary preamble into the various PartialSave attributes.'''
-
-        self.size_in_bytes, self.total_frames, self.compression_enabled, self.encryption_enabled, self.masking_enabled,\
-        self.custom_palette_used, self.date_created, self.stream_palette_id, self.ascii_header_byte_size = \
-            decode_stream_header_binary_preamble(self.stream_header_preamble_buffer)
-        self.stream_header_preamble_buffer = None
-        self.date_created = datetime.datetime.fromtimestamp(int(self.date_created)).strftime('%Y-%m-%d %H:%M:%S')
-
-        logging.info(f'*** Part 1/2 of header decoded for {self.stream_sha}: ***\nPayload size: {self.size_in_bytes} B'
-                     f'\nTotal frames: {self.total_frames}\nCompression enabled: {self.compression_enabled}'
-                     f'\nEncryption Enabled: {self.encryption_enabled}\nFile masking enabled: {self.masking_enabled}'
-                     f'\nCustom Palette Used: {self.custom_palette_used}\nDate created: {self.date_created}'
-                     f'\nStream palette ID: {self.stream_palette_id}')
-        logging.debug(f'ASCII header byte size: {self.ascii_header_byte_size} B')
-
-        self._create_frame_reference_table()
-        self.stream_header_preamble_complete = True
-
-
-    def read_stream_header_ascii_compressed(self):
-        '''This method converts the raw full ASCII stream header into the various PartialSave attributes.'''
-
-        self.bg_version, self.stream_name, self.stream_description, self.file_list, self.custom_color_name, \
-        self.custom_color_description, self.custom_color_date_created, self.custom_color_palette, \
-        self.post_compression_sha = \
-         decode_stream_header_ascii_compressed(self.stream_header_ascii_buffer, self.custom_palette_used,
-                                               self.encryption_enabled)
-        self.stream_header_ascii_buffer = None
-
-        if self.masking_enabled == True:
-            self.file_list = "Cannot display, file masking enabled for this stream!"
-
-        else:
-            self.file_list = format_file_list(self.file_list)
-
-        logging.info(f'*** Part 2/2 of header decoded for {self.stream_sha}: ***\nProgram version of sender: '
-                     f'v{self.bg_version}\nStream name: {self.stream_name}\nStream description: '
-                     f'{self.stream_description} \nFile list: {self.file_list}')
-
-        if self.custom_palette_used == True:
-            logging.info(f'\nCustom color name: {self.custom_color_name}\nCustom color description: '
-                         f'{self.custom_color_description}\nCustom color date created: '
-                    f'{datetime.datetime.fromtimestamp(int(self.custom_color_date_created)).strftime("%Y-%m-%d %H:%M:%S")}'
-                         f'\nCustom color palette: {self.custom_color_palette}')
-
-        if self.encryption_enabled == True:
-            logging.info(f'Post-compression hash: {self.post_compression_sha}')
-            self.assembled_sha = self.post_compression_sha
-
-        self.stream_header_ascii_complete = True
-
-
-    def is_frame_needed(self, frame_number):
-        '''This determines whether a given frame is needed for this stream or not.'''
-
-        # Is the stream already assembled?  If so, no more frames need to be accepted.
-        if self.is_assembled == True:
-            return False
-
-        # The stream is not assembled.
-        else:
-
-            # If the stream header binary preamble isn't loaded yet, then by default we accept the frame, unless that
-            # frame number is already in self.frames_prior_to_binary_preamble, which is a list of processed frames prior
-            # to the binary preamble being read.
-            if self.stream_header_preamble_complete == False:
-                if frame_number not in self.frames_prior_to_binary_preamble:
-                    return True
-
-                else:
-                    return False
-
-            # The preamble has been loaded, checking the reference table.
-            else:
-
-                # Frame reference table is not in memory and must be loaded.
-                if self.frame_reference_table == None:
-
-                    self.frame_reference_table = BitStream(self._read_file('\\frame_reference_table',
-                                                                           to_decompress=True))
-
-                self.frame_reference_table.bitpos = frame_number - 1
-                isFrameLoaded = self.frame_reference_table.read('bool')
-                return not isFrameLoaded
-
-
-    def _close_session(self):
-        '''Called by the Assembler object, this will flush self.frame_reference_table back to disk, as well as flag it as
-        an inactive session.
-        '''
-
-        self.active_this_session = False
-
-        if self.stream_header_preamble_complete == True:
-            self._write_file(self.frame_reference_table, '\\frame_reference_table', to_compress=True)
-
-        self.frame_reference_table = None
-        logging.debug(f'Closing session for {self.stream_sha}')
-
-
-    def return_status(self, debug_data):
-        '''This is used in print_full_save_list in savedfunctions module; it returns the state of the object, as well as
-        various information read from the reader.  __str__ is not used, as debug_data controls what level of data is
-        returned, whether for end users, or debugging purposes.
-        '''
-
-        temp_holder = f"{'*' * 8} {self.stream_sha} {'*' * 8}" \
-            f"\n\n{self.frames_ingested} / {self.total_frames} frames saved" \
-            f"\n\nStream header complete: {self.stream_header_ascii_complete}" \
-            f"\nIs assembled: {self.is_assembled}" \
-            f"\n\nStream name: {self.stream_name}" \
-            f"\nStream Description: {self.stream_description}" \
-            f"\nDate Created: {self.date_created}" \
-            f"\nSize in bytes: {self.size_in_bytes} B" \
-            f"\nCompression enabled: {self.compression_enabled}" \
-            f"\nEncryption enabled: {self.encryption_enabled}" \
-            f"\nFile masking enabled: {self.masking_enabled}" \
-            f"\n\nEncryption key: {self.encryption_key} " \
-            f"\nScrypt N: {self.scrypt_n}" \
-            f"\nScrypt R: {self.scrypt_r}" \
-            f"\nScrypt P: {self.scrypt_p}" \
-            f"\nOutput path upon assembly: {self.output_path}" \
-            f"\n\nFile list: {self.file_list}"
-
-        if debug_data == True:
-            temp_holder += f"\n\n{'*' * 3} {'DEBUG'} {'*' * 3}" \
-                f"\nBG version: {self.bg_version}" \
-                f"\nASCII header byte size: {self.ascii_header_byte_size}" \
-                f"\nPost compression SHA: {self.post_compression_sha}" \
-                f"\nStream Palette ID: {self.stream_palette_id}" \
-                f"\n\nCustom color data (if applicable)" \
-                f"\nCustom color name: {self.custom_color_name}" \
-                f"\nCustom color description: {self.custom_color_description}" \
-                f"\nCustom color date created: {self.custom_color_date_created}" \
-                f"\nCustom color palette: {self.custom_color_palette}"
-
-        return temp_holder
-
-
-    def return_stream_header_id(self):
-        '''This method releases the stream_palette ID (and custom color data if applicable) for the Decoder to use to
-        create the palette.
-        '''
-
-        return self.stream_header_ascii_complete, self.stream_palette_id, self.custom_color_name, \
-               self.custom_color_description,  self.custom_color_date_created, self.custom_color_palette
\ No newline at end of file
diff --git a/bitglitter/read/partialsaveassets.py b/bitglitter/read/partialsaveassets.py
deleted file mode 100644
index 6d2c598..0000000
--- a/bitglitter/read/partialsaveassets.py
+++ /dev/null
@@ -1,75 +0,0 @@
-import ast
-import logging
-import zlib
-
-
-def decode_stream_header_ascii_compressed(bitstream, custom_color_enabled, encryption_enabled):
-    '''This function encodes the raw bit string taken from the frame(s) back into ASCII, and returns the split
-    components inside of it.
-    '''
-
-    logging.debug('Reading stream header...')
-    custom_color_name = None
-    custom_color_description = None
-    custom_color_date_created = None
-    custom_color_palette = None
-    post_compression_sha = None
-
-    to_bytes = bitstream.tobytes()
-    logging.debug(f'ASCII header byte size inputted to read function: {int(len(bitstream) / 8)} B')
-    uncompressed_string = zlib.decompress(to_bytes).decode()
-    string_broken_into_parts = uncompressed_string.split('\\\\')[1:-1]
-    bg_version = string_broken_into_parts[0]
-    stream_name = string_broken_into_parts[1]
-    stream_description = string_broken_into_parts[2]
-    file_list = string_broken_into_parts[3]
-
-    index_bump = 0
-    if custom_color_enabled == True:
-        custom_color_name = string_broken_into_parts[4]
-        custom_color_description = string_broken_into_parts[5]
-        custom_color_date_created = string_broken_into_parts[6]
-        custom_color_palette = ast.literal_eval(string_broken_into_parts[7])
-        index_bump += 4
-
-    if encryption_enabled == True:
-        post_compression_sha = string_broken_into_parts[4 + index_bump]
-
-    logging.debug('Stream header ASCII part successfully read.')
-
-    return bg_version,stream_name,stream_description,file_list,custom_color_name,custom_color_description,\
-           custom_color_date_created,custom_color_palette, post_compression_sha
-
-
-def decode_stream_header_binary_preamble(bitStream):
-    '''This function takes the raw bit string taken from the frame(s) and extracts stream data from it.'''
-
-    size_in_bytes = bitStream.read('uint : 64')
-    total_frames = bitStream.read('uint : 32')
-    compression_enabled = bitStream.read('bool')
-    encryption_enabled = bitStream.read('bool')
-    file_masking_enabled = bitStream.read('bool')
-    is_custom_palette = bitStream.read('bool')
-    date_created = bitStream.read('uint : 34')
-
-    if is_custom_palette == False:
-        stream_palette_id = str(bitStream.read('uint : 256'))
-
-    else:
-        stream_palette_id = str(bitStream.read('hex : 256'))
-
-    ascii_header_compressed_in_bytes = bitStream.read('uint : 32')
-
-    return size_in_bytes, total_frames, compression_enabled, encryption_enabled, file_masking_enabled, \
-           is_custom_palette,  date_created, stream_palette_id, ascii_header_compressed_in_bytes
-
-def format_file_list(file_string):
-    '''This takes in the file manifest inside of the stream header, and prints it in a nice formatted way.'''
-
-    broken_apart = file_string.split('|')[1:]
-    formatted_string = ''
-
-    for position in range(int(len(broken_apart) / 2)):
-        formatted_string +=(f"\n    {broken_apart[2 * position]} - {broken_apart[2 * position + 1]} B")
-
-    return formatted_string
\ No newline at end of file
diff --git a/bitglitter/read/process_state/__init__.py b/bitglitter/read/process_state/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/bitglitter/read/process_state/framereadhandler.py b/bitglitter/read/process_state/framereadhandler.py
new file mode 100644
index 0000000..810a6bc
--- /dev/null
+++ b/bitglitter/read/process_state/framereadhandler.py
@@ -0,0 +1,207 @@
+from sqlalchemy.exc import InvalidRequestError
+
+import logging
+from multiprocessing import cpu_count, Pool
+from pathlib import Path
+
+from bitglitter.config.palettemodels import Palette
+from bitglitter.config.readmodels.streamread import StreamRead
+from bitglitter.read.process_state.videoframegenerator import video_frame_generator
+from bitglitter.read.process_state.imageframeprocessor import ImageFrameProcessor
+from bitglitter.read.process_state.multiprocess_state_generator import image_state_generator, video_state_generator
+from bitglitter.read.process_state.videoframeprocessor import VideoFrameProcessor
+from bitglitter.utilities.read import flush_inactive_frames
+
+
+def frame_read_handler(input_path, output_directory, input_type, bad_frame_strikes, max_cpu_cores,
+                       block_height_override, block_width_override, decryption_key, scrypt_n, scrypt_r, scrypt_p,
+                       temp_save_directory, stop_at_metadata_load, auto_unpackage_stream, auto_delete_finished_stream,
+                       save_statistics, valid_image_formats):
+    logging.info(f'Processing {input_path}...')
+
+    #  Initializing variables that will be in all frame_process() calls
+    initializer_palette_a = Palette.query.filter(Palette.palette_id == '1').first()
+    initializer_palette_b = Palette.query.filter(Palette.palette_id == '11').first()
+    initializer_palette_a_color_set = initializer_palette_a.convert_colors_to_tuple()
+    initializer_palette_b_color_set = initializer_palette_b.convert_colors_to_tuple()
+    initializer_palette_a_dict = initializer_palette_a.return_decoder()
+    initializer_palette_b_dict = initializer_palette_b.return_decoder()
+
+    stream_palette = None
+    stream_palette_dict = None
+    stream_palette_color_set = None
+
+    initial_state_dict = {'output_directory': output_directory, 'block_height_override': block_height_override,
+                          'block_width_override': block_width_override, 'decryption_key': decryption_key, 'scrypt_n':
+                          scrypt_n, 'scrypt_r': scrypt_r, 'scrypt_p': scrypt_p, 'temp_save_directory':
+                          temp_save_directory, 'initializer_palette_a': initializer_palette_a,
+                          'initializer_palette_a_color_set': initializer_palette_a_color_set,
+                          'initializer_palette_b_color_set': initializer_palette_b_color_set,
+                          'initializer_palette_a_dict': initializer_palette_a_dict, 'initializer_palette_b_dict':
+                          initializer_palette_b_dict, 'auto_delete_finished_stream': auto_delete_finished_stream,
+                          'stop_at_metadata_load': stop_at_metadata_load, 'save_statistics': save_statistics,
+                          'auto_unpackage_stream': auto_unpackage_stream, 'sequential': True}
+
+    # Multicore setup
+    if max_cpu_cores == 0 or max_cpu_cores >= cpu_count():
+        cpu_pool_size = cpu_count()
+    else:
+        cpu_pool_size = max_cpu_cores
+
+    frame_strikes_this_session = 0
+    image_strike_limit_hit = False
+    image_metadata_checkpoint_data = None
+    frame_read_results = {'active_sessions_this_stream': []}
+
+    if input_type == 'video':
+
+        frame_generator = video_frame_generator(input_path)
+        total_video_frames = next(frame_generator)
+        initial_state_dict['total_frames'] = total_video_frames
+        logging.info(f'{total_video_frames} frame(s) detected in video file.')
+        initial_state_dict['mode'] = 'video'
+
+        # Processing frames in a single process until all metadata has been received, then switch to multicore
+        logging.info('Starting single core sequential decoding until metadata captured...')
+        for frame_data in frame_generator:
+            initial_state_dict['frame'] = frame_data['frame']
+            initial_state_dict['current_frame_position'] = frame_data['current_frame_position']
+            video_frame_processor = VideoFrameProcessor(initial_state_dict)
+
+            # Skip if all frames completed
+            if video_frame_processor.skip_process:
+                frame_read_results['active_sessions_this_stream'].append(video_frame_processor.stream_sha256)
+                break
+
+            # Errors
+            if 'error' in video_frame_processor.frame_errors:
+                # Session-ending error, such as a metadata frame being corrupted
+                if 'fatal' in video_frame_processor.frame_errors:
+                    logging.warning('Cannot continue.')
+                    return {'error': True}
+                if bad_frame_strikes:  # Corrupted frame, skipping to next one
+                    frame_strikes_this_session += 1
+                    logging.warning(f'Bad frame strike {frame_strikes_this_session}/{bad_frame_strikes}')
+                    if frame_strikes_this_session >= bad_frame_strikes:
+                        logging.warning('Reached frame strike limit.  Aborting...')
+                        return {'error': True}
+
+            if frame_data['current_frame_position'] == 1:
+                stream_read = video_frame_processor.stream_read
+                initial_state_dict['stream_read'] = stream_read
+
+            # Metadata return
+            if video_frame_processor.metadata and stream_read.stop_at_metadata_load:
+                return {'metadata': video_frame_processor.metadata}
+
+            # Stream palette load
+            if video_frame_processor.stream_palette and video_frame_processor.stream_palette_loaded_this_frame:
+                stream_palette = video_frame_processor.stream_palette
+                stream_palette_dict = video_frame_processor.stream_palette_dict
+                stream_palette_color_set = video_frame_processor.stream_palette_color_set
+                initial_state_dict['stream_palette'] = stream_palette
+                initial_state_dict['stream_palette_dict'] = stream_palette_dict
+                initial_state_dict['stream_palette_color_set'] = stream_palette_color_set
+
+            # Headers are decoded, can switch to multiprocessing
+            if stream_read.palette_header_complete and stream_read.metadata_header_complete:
+                frame_read_results['active_sessions_this_stream']\
+                    .append(video_frame_processor.stream_read.stream_sha256)
+                break
+
+        # Begin multicore frame decode
+        if not video_frame_processor.skip_process:
+            with Pool(processes=cpu_pool_size) as worker_pool:
+                logging.info(f'Metadata headers fully decoded, now decoding on {cpu_pool_size} CPU core(s)...')
+                for multicore_read_results in worker_pool.imap(VideoFrameProcessor,
+                                                           video_state_generator(frame_generator, stream_read,
+                                                           save_statistics, initializer_palette_a,
+                                                           initializer_palette_a_dict, initializer_palette_a_color_set,
+                                                           total_video_frames, stream_palette, stream_palette_dict,
+                                                           stream_palette_color_set)):
+
+                    if 'error' in multicore_read_results.frame_errors:
+                        if bad_frame_strikes:  # Corrupted frame, skipping to next one
+                            frame_strikes_this_session += 1
+                            logging.warning(f'Bad frame strike {frame_strikes_this_session}/{bad_frame_strikes}')
+                            if frame_strikes_this_session >= bad_frame_strikes:
+                                logging.warning('Reached frame strike limit.  Aborting...')
+                                return {'error': True}
+
+    elif input_type == 'image':
+
+        # Normalizing the different forms of input path into a common list format format
+        if isinstance(input_path, list):
+            input_list = input_path
+        elif isinstance(input_path, str):
+            path = Path(input_path)
+            if path.is_dir():
+                input_list = []
+                for file_format in valid_image_formats:
+                    for whitelisted_file in path.rglob(file_format):
+                        input_list.append(str(whitelisted_file))
+            else:
+                input_list = [input_path]
+
+        # Begin multicore frame decode
+        image_metadata_checkpoint_data = None
+        image_strike_limit_hit = False
+        with Pool(processes=cpu_pool_size) as worker_pool:
+            logging.info(f'Decoding on {cpu_pool_size} CPU core(s)...')
+            for multicore_read_results in worker_pool.imap(ImageFrameProcessor, image_state_generator(input_list,
+                                                                                                  initial_state_dict)):
+                if 'error' in multicore_read_results.frame_errors:
+                    if bad_frame_strikes:  # Corrupted frame, skipping to next one
+                        frame_strikes_this_session += 1
+                        logging.warning(f'Bad frame strike {frame_strikes_this_session}/{bad_frame_strikes}'
+                                        f' ({multicore_read_results.file_name})')
+                        if frame_strikes_this_session >= bad_frame_strikes:
+                            logging.warning('Reached frame strike limit.  Aborting...')
+                            image_strike_limit_hit = True
+                            break
+
+                if multicore_read_results.stream_sha256:
+                    if multicore_read_results.stream_sha256 not in frame_read_results['active_sessions_this_stream']:
+                        frame_read_results['active_sessions_this_stream'].append(multicore_read_results.stream_sha256)
+
+                if multicore_read_results.metadata and multicore_read_results.stream_read.stop_at_metadata_load:
+                    image_metadata_checkpoint_data = multicore_read_results.metadata
+                    break
+
+    # Outside the scope of image multiprocessing to work properly
+    if image_strike_limit_hit:
+        return {'error': True}
+    if image_metadata_checkpoint_data:
+        return {'metadata': image_metadata_checkpoint_data}
+
+    logging.info('Frame scanning complete.')
+
+    #  Remove incomplete frames from db
+    flush_inactive_frames()
+
+    # Closing active sessions and unpackaging streams if its set to:
+    extracted_file_count = 0
+    active_reads_this_session = StreamRead.query.filter(StreamRead.active_this_session == True).all()
+    unpackaging_this_session = False
+    if active_reads_this_session:
+        logging.info(f'{len(active_reads_this_session)} active stream(s) this session.')
+        frame_read_results['unpackage_results'] = {}
+        for stream_read in active_reads_this_session:
+            stream_read.completed_frame_count_update()
+            if stream_read.auto_unpackage_stream:
+                unpackaging_this_session = True
+                unpackage_results, total_unpackaged = stream_read.attempt_unpackage(temp_save_directory)
+                extracted_file_count += total_unpackaged
+                frame_read_results['unpackage_results'][stream_read.stream_sha256] = unpackage_results
+                stream_read.autodelete_attempt()
+            else:
+                stream_read.check_file_eligibility()
+            try:
+                stream_read.session_activity(False)
+            except InvalidRequestError:
+                pass
+        if unpackaging_this_session:
+            logging.info('File unpackaging complete.')
+    frame_read_results['extracted_file_count'] = extracted_file_count
+
+    return {'frame_read_results': frame_read_results}
diff --git a/bitglitter/read/process_state/imageframeprocessor.py b/bitglitter/read/process_state/imageframeprocessor.py
new file mode 100644
index 0000000..3ec7e08
--- /dev/null
+++ b/bitglitter/read/process_state/imageframeprocessor.py
@@ -0,0 +1,495 @@
+from bitstring import BitStream
+
+import logging
+
+from bitglitter.config.configfunctions import read_stats_update
+from bitglitter.config.palettefunctions import import_custom_palette_from_header
+from bitglitter.config.readmodels.streamread import StreamRead
+from bitglitter.config.readmodels.readmodels import StreamFrame, StreamSHA256Blacklist
+from bitglitter.read.decode.headerdecode import custom_palette_header_validate_decode, frame_header_decode, \
+    initializer_header_validate_decode, metadata_header_validate_decode, stream_header_decode
+from bitglitter.read.scan.scanvalidate import frame_lock_on, geometry_override_checkpoint
+from bitglitter.read.scan.scanhandler import ScanHandler
+from bitglitter.utilities.cryptography import get_sha256_hash_from_bytes
+
+
+class ImageFrameProcessor:
+    def __init__(self, dict_obj):
+        frame_position = dict_obj['current_frame_position']
+        total_frames = dict_obj['total_frames']
+        self.file_name = dict_obj['file_name']
+        percentage_string = f'{round(((frame_position / total_frames) * 100), 2):.2f}'
+        logging.info(f"Processing \"{self.file_name}\" - {frame_position} of {total_frames}... {percentage_string} %")
+
+        self.starting_state = dict_obj['starting_state']
+
+        self.ERROR_SOFT = {'error': True}  # Only this frame is cancelled, decoding can continue on next frame
+        self.ERROR_BREAK = {'break': True}  # Not corruption related, some condition that requires skipping instead
+        self.frame_errors = {}
+
+        # State
+        self.is_unique_frame = False
+        self.frame_hashable_bits = BitStream()
+        self.stream_payload_bits = BitStream()
+        self.frame_blocks_left = True
+        self.payload_in_frame = False
+        self.setup_headers_end_this_frame = False
+        self.save_statistics = self.starting_state['save_statistics']
+
+        self.stream_read = None
+        self.scan_handler = None
+        self.stream_frame = None
+        self.stream_sha256 = None
+        self.frame_sha256 = None
+        self.frame_number = None
+        self.metadata = None
+        self.metadata_header_bytes = None
+        self.palette_header_bytes = None
+        self.bits_to_read = None
+
+        # Setup
+        self.frame = dict_obj['frame']
+        self.frame_pixel_height = self.frame.shape[0]
+        self.frame_pixel_width = self.frame.shape[1]
+        self.initializer_palette_a = self.starting_state['initializer_palette_a']
+        self.initializer_palette_a_color_set = self.starting_state['initializer_palette_a_color_set']
+        self.initializer_palette_a_dict = self.starting_state['initializer_palette_a_dict']
+        self.initializer_palette_b_color_set = self.starting_state['initializer_palette_b_color_set']
+        self.initializer_palette_b_dict = self.starting_state['initializer_palette_b_dict']
+
+        self.output_directory = self.starting_state['output_directory']
+        self.block_height_override = self.starting_state['block_height_override']
+        self.block_width_override = self.starting_state['block_width_override']
+        self.decryption_key = self.starting_state['decryption_key']
+        self.scrypt_n = self.starting_state['scrypt_n']
+        self.scrypt_r = self.starting_state['scrypt_r']
+        self.scrypt_p = self.starting_state['scrypt_p']
+        self.temp_save_directory = self.starting_state['temp_save_directory']
+        self.auto_delete_finished_stream = self.starting_state['auto_delete_finished_stream']
+        self.auto_unpackage_stream = self.starting_state['auto_unpackage_stream']
+        self.stop_at_metadata_load = self.starting_state['stop_at_metadata_load']
+
+        self.block_height = None
+        self.block_width = None
+        self.pixel_width = None
+
+        self.stream_palette = None
+        self.stream_palette_id = None
+        self.stream_palette_dict = None
+        self.stream_palette_color_set = None
+
+        self._initial_frame_setup()
+        self._frame_header_process()
+        if self.frame_number == 1:
+            self._stream_header_process()
+        if self.stream_read and self.frame_number:
+            if not self.stream_read.is_payload_ready() and \
+                    self.frame_number - 1 == self.stream_read.highest_consecutive_setup_frame_read:
+                if not self.stream_read.metadata_header_complete:
+                    self._metadata_header_process_attempt()
+                if not self.stream_read.palette_header_complete:
+                    self._palette_header_process_attempt()
+        self._payload_process()
+        self._frame_validation()
+        self._metadata_checkpoint()
+        self._run_statistics()
+        self.scan_handler = None  # Keep as is, multiprocessing cannot return its internal generator and crashes
+        logging.debug('Frame decode cycle complete.')
+
+    def _initial_frame_setup(self):
+        # Geometry override checkpoint
+        if not geometry_override_checkpoint(self.block_height_override, self.block_width_override,
+                                            self.frame_pixel_height, self.frame_pixel_width):
+            self.frame_errors = self.ERROR_SOFT
+            return
+
+        # Geometry override checkpoint
+        if not geometry_override_checkpoint(self.block_height_override, self.block_width_override,
+                                            self.frame_pixel_height, self.frame_pixel_width):
+            self.frame_errors = self.ERROR_SOFT
+            return
+
+        # Frame lock on
+        lock_on_results = frame_lock_on(self.frame, self.block_height_override, self.block_width_override,
+                                        self.frame_pixel_height, self.frame_pixel_width,
+                                        self.initializer_palette_a_color_set, self.initializer_palette_b_color_set,
+                                        self.initializer_palette_a_dict, self.initializer_palette_b_dict)
+        if not lock_on_results:
+            self.frame_errors = self.ERROR_SOFT
+            return
+        self.block_height = lock_on_results['block_height']
+        self.block_width = lock_on_results['block_width']
+        self.pixel_width = lock_on_results['pixel_width']
+
+        # Now we passed minimum frame validation, moving to ScanHandler setup
+        self.scan_handler = ScanHandler(self.frame, True, self.initializer_palette_a, self.initializer_palette_a_dict,
+                                        self.initializer_palette_a_color_set, self.block_height, self.block_width,
+                                        self.pixel_width)
+
+        #  Initializer scan and decode
+        initializer_results = self.scan_handler.return_initializer_bits()
+        if 'error' in initializer_results:
+            self.frame_errors = self.ERROR_SOFT
+            return
+        initializer_bits_raw = initializer_results['bits']
+        initializer_decode_results = initializer_header_validate_decode(initializer_bits_raw, self.block_height,
+                                                                        self.block_width)
+        if 'error' in initializer_decode_results:
+            self.frame_errors = self.ERROR_SOFT
+            return
+
+        protocol_version = initializer_decode_results['protocol_version']
+        custom_palette_used = initializer_decode_results['custom_palette_used']
+        self.stream_sha256 = initializer_decode_results['stream_sha256']
+
+        self.palette_header_complete = False
+        if initializer_decode_results['palette']:  # Palette already stored in db, not pending in future header
+            self.stream_palette = initializer_decode_results['palette']
+            self.stream_palette_dict = self.stream_palette.return_decoder()
+            self.stream_palette_color_set = self.stream_palette.convert_colors_to_tuple()
+            self.scan_handler.set_stream_palette(self.stream_palette, self.stream_palette_dict,
+                                                 self.stream_palette_color_set)
+            self.palette_header_complete = not custom_palette_used
+            self.stream_palette_loaded_this_frame = True
+            custom_palette_loaded = True
+        else:
+            self.stream_palette_id = initializer_decode_results['stream_palette_id']
+            custom_palette_loaded = False
+
+        # Blacklist check
+        blacklisted_hash = StreamSHA256Blacklist.query.filter(StreamSHA256Blacklist == self.stream_sha256).first()
+        if blacklisted_hash:
+            logging.warning(f'Hash {self.stream_sha256} is on your blacklist.  Aborting...')
+            self.frame_errors = self.ERROR_SOFT
+            return
+
+        # Loading or creating StreamRead instance
+        self.stream_read = StreamRead.query.filter(StreamRead.stream_sha256 == self.stream_sha256).first()
+        if self.stream_read:
+            if self.stream_read.is_complete:
+                logging.info(f'{self.stream_read.stream_name} -- {self.stream_sha256} is complete.  Aborting...')
+                self.frame_errors = self.ERROR_BREAK
+                return
+            else:
+                self.stream_read.session_activity(True, save=False)
+                self.stream_read.toggle_eligibility_calculations(True)
+            logging.info(f'Existing stream read found: {self.stream_read}')
+            if self.stream_read.all_frames_accounted_for:
+                logging.info('All frames accounted for, skipping video frame processing...')
+                self.frame_errors = self.ERROR_SOFT
+                return
+
+        else:
+            logging.info(f'New stream: {self.stream_sha256}')
+            self.stream_read = StreamRead.create(stream_sha256=self.stream_sha256, stream_is_video=False,
+                                                 protocol_version=protocol_version, output_directory=
+                                                 self.output_directory, decryption_key=self.decryption_key, scrypt_n=
+                                                 self.scrypt_n, scrypt_r=self.scrypt_r, scrypt_p=self.scrypt_p,
+                                                 auto_delete_finished_stream=self.auto_delete_finished_stream,
+                                                 stop_at_metadata_load=self.stop_at_metadata_load,
+                                                 palette_header_complete=self.palette_header_complete,
+                                                 auto_unpackage_stream=self.auto_unpackage_stream, block_height=
+                                                 self.block_height, block_width=self.block_width, pixel_width=
+                                                 self.pixel_width, stream_palette_id=self.stream_palette_id,
+                                                 custom_palette_used=custom_palette_used, custom_palette_loaded=
+                                                 custom_palette_loaded)
+            if self.stream_palette:
+                self.stream_read.stream_palette_load(self.stream_palette)
+
+    def _frame_header_process(self):
+
+        if self.frame_errors:
+            return
+
+        if self.stream_read.is_payload_ready():
+            is_initializer_palette = False
+        else:
+            is_initializer_palette = True
+
+        frame_header_bits_raw = self.scan_handler.return_frame_header_bits(is_initializer_palette=
+                                                                           is_initializer_palette)['bits']
+        frame_header_decode_results = frame_header_decode(frame_header_bits_raw)
+        if not frame_header_decode_results:
+            if is_initializer_palette:
+                if not self.stream_palette:
+                    logging.info('Frame header not read yet, cannot read this frame.  Aborting frame...')
+                logging.info('Frame header fail, falling back to stream palette to attempt to read...')
+                frame_header_bits_raw = self.scan_handler.return_frame_header_bits(is_initializer_palette=False,
+                                                                                   redo=True)['bits']
+                frame_header_decode_results = frame_header_decode(frame_header_bits_raw)
+
+                if not frame_header_decode_results:
+                    logging.info('That didn\'t work, aborting frame...')
+                    self.frame_errors = self.ERROR_SOFT
+                    return
+                else:
+                    logging.info('That worked.')
+            else:
+                self.frame_errors = self.ERROR_SOFT
+                return
+        self.frame_sha256 = frame_header_decode_results['frame_sha256']
+        self.frame_number = frame_header_decode_results['frame_number']
+        self.bits_to_read = frame_header_decode_results['bits_to_read']
+        self.scan_handler.set_bits_to_read(self.bits_to_read)
+
+        # Breaking out of frame if non-sequential frame while setting up stream, and a custom palette is used not in DB
+        if not self.stream_read.is_payload_ready() and self.stream_read.custom_palette_used and \
+            not self.stream_read.custom_palette_loaded:
+            if self.frame_number - 1 != self.stream_read.highest_consecutive_setup_frame_read:
+                logging.warning('Cannot read frame, as palette header (in the first few frames) hasn\' yet been'
+                                'read/decoded.  Aborting frame...')
+                self.frame_errors = self.ERROR_SOFT
+                return
+
+        # Checking if frame exists
+        self.stream_frame = StreamFrame.query.filter(StreamFrame.stream_id == self.stream_read.id) \
+            .filter(StreamFrame.frame_number == self.frame_number).first()
+        if self.stream_frame:  # Frame already loaded, skipping
+            if self.stream_frame.is_complete:  # Current frame is fully validated and saved
+                logging.info(f'Frame {self.frame_number} is already complete')
+            else:  # Current frame is being actively processed by another process
+                logging.debug(f'Pending active frame in another process: {self.frame_number}')
+            self.frame_errors = self.ERROR_BREAK
+            self.frame_blocks_left = False
+            return
+        else:  # New frame
+            self.stream_frame = StreamFrame.create(stream_id=self.stream_read.id, frame_number=self.frame_number)
+            logging.debug(f'New frame: #{self.frame_number}')
+            self.is_unique_frame = True
+
+    def _stream_header_process(self):
+        if self.frame_errors:
+            return
+        if self.frame_blocks_left:
+            stream_header_results = self.scan_handler.return_stream_header_bits(is_initializer_palette=True)
+
+            # Note- stream header always fits in first frame with current protocol, hence the missing carry over logic.
+            stream_header_bits_raw = stream_header_results['bits']
+            self.frame_hashable_bits += stream_header_results['bits']
+            stream_header_decode_results = stream_header_decode(stream_header_bits_raw)
+            if not stream_header_decode_results:
+                self.frame_errors = self.ERROR_SOFT
+                return
+            logging.debug(f'{stream_header_decode_results=}')
+            size_in_bytes = stream_header_decode_results['size_in_bytes']
+            total_frames = stream_header_decode_results['total_frames']
+            compression_enabled = stream_header_decode_results['compression_enabled']
+            encryption_enabled = stream_header_decode_results['encryption_enabled']
+            file_masking_enabled = stream_header_decode_results['file_masking_enabled']
+            metadata_header_length = stream_header_decode_results['metadata_header_length']
+            metadata_header_hash = stream_header_decode_results['metadata_header_hash']
+            custom_palette_header_length = stream_header_decode_results['custom_palette_header_length']
+            custom_palette_header_hash = stream_header_decode_results['custom_palette_header_hash']
+            self.stream_read.stream_header_load(size_in_bytes, total_frames, compression_enabled,
+                                                encryption_enabled, file_masking_enabled, metadata_header_length,
+                                                metadata_header_hash, custom_palette_header_length,
+                                                custom_palette_header_hash)
+
+    def _metadata_header_process_attempt(self):
+        def _metadata_header_decode():
+            """Actual header processing once all bytes are accounted for."""
+
+            metadata_header_decode_results = metadata_header_validate_decode(self.metadata_header_bytes,
+                                                                             self.stream_read.metadata_header_sha256_raw,
+                                                                             self.stream_read.decryption_key,
+                                                                             self.stream_read.encryption_enabled,
+                                                                             self.stream_read.file_masking_enabled,
+                                                                             self.stream_read.scrypt_n,
+                                                                             self.stream_read.scrypt_r,
+                                                                             self.stream_read.scrypt_p)
+            logging.debug(f'{metadata_header_decode_results=}')
+
+            # Incorrect decryption key, storing header bytes in stream read
+            if metadata_header_decode_results == None:
+                self.stream_read.accept_encrypted_metadata_bytes(self.metadata_header_bytes)
+
+            # Corrupted header
+            elif metadata_header_decode_results == False:
+                self.frame_errors = self.ERROR_SOFT
+                return
+
+            # Successful and decode (and successful decryption if applicable)
+            elif 'bg_version' in metadata_header_decode_results:
+                bg_version = metadata_header_decode_results['bg_version']
+                stream_name = metadata_header_decode_results['stream_name']
+                stream_description = metadata_header_decode_results['stream_description']
+                time_created = metadata_header_decode_results['time_created']
+                manifest_string = metadata_header_decode_results['manifest_string']
+                self.stream_read.metadata_header_load(bg_version, stream_name, stream_description, time_created,
+                                                      manifest_string)
+
+        if self.frame_errors:
+            return
+        if self.frame_blocks_left:
+
+            if self.stream_read.carry_over_header_bytes:  # Loading in previous bytes from last frame to re-attempt
+                metadata_header_bits = self.stream_read.return_pending_header_bits()
+                scan_length = (self.stream_read.metadata_header_size_bytes * 8) - metadata_header_bits.len
+                metadata_scan_results = self.scan_handler.return_bits(scan_length, is_initializer_palette=True,
+                                                                      is_payload=True, byte_input=False)
+                metadata_header_bits += metadata_scan_results['bits']
+
+                if metadata_scan_results['complete_request']:  # Header terminates here
+                    logging.debug('Metadata header terminates this frame.')
+                    self.metadata_header_bytes = metadata_header_bits.tobytes()
+                    self.frame_hashable_bits += metadata_scan_results['bits']
+                    self.setup_headers_end_this_frame = True if not self.stream_read.custom_palette_used else False
+                    if self.stream_read.decryption_key and self.stream_read.file_masking_enabled or \
+                            not self.stream_read.file_masking_enabled:
+                        _metadata_header_decode()
+                        self.stream_read.set_pending_header_bits()
+                    else:
+                        logging.info('This stream has file masking enabled, and no decryption key was provided.  Cannot'
+                                     ' open metadata header until a decryption key (and a proper one) is provided!')
+                        self.stream_read.accept_encrypted_metadata_bytes(self.metadata_header_bytes)
+                        return
+                else:  # Header carries over into next frame
+                    logging.debug('Metadata header rolling over to next frame...')
+                    self.stream_read.set_pending_header_bits(metadata_header_bits)
+                    self.frame_blocks_left = False
+                    self.frame_hashable_bits += metadata_scan_results['bits']
+                    return
+
+            else:  # First frame
+                metadata_scan_results = self.scan_handler.return_bits(self.stream_read.metadata_header_size_bytes,
+                                                                      is_initializer_palette=True, is_payload=True,
+                                                                      byte_input=True)
+                metadata_header_bits = metadata_scan_results['bits']
+                if not metadata_scan_results['complete_request']:
+                    logging.debug('Metadata header rolling over to next frame...')
+                    self.stream_read.set_pending_header_bits(metadata_header_bits)
+                    self.frame_blocks_left = False
+                    self.frame_hashable_bits += metadata_header_bits
+                    return
+                self.metadata_header_bytes = metadata_header_bits.tobytes()
+                self.frame_hashable_bits += self.metadata_header_bytes
+                _metadata_header_decode()
+                self.setup_headers_end_this_frame = True if not self.stream_read.custom_palette_used else False
+
+    def _palette_header_process_attempt(self):
+        def _palette_header_decode():
+            """Actual header processing once all bytes are accounted for."""
+            self.stream_read.palette_header_complete = True
+            self.stream_read.save()
+
+            if self.stream_read.custom_palette_loaded or not self.stream_read.custom_palette_used:
+                return
+
+            palette_header_decode_results = custom_palette_header_validate_decode(self.palette_header_bytes,
+                                                                              self.stream_read.palette_header_sha256)
+            logging.debug(f'{palette_header_decode_results=}')
+
+            # Header integrity error
+            if palette_header_decode_results == False:
+                self.frame_errors = self.ERROR_SOFT
+                return
+
+            # Successfully decoded
+            elif 'palette_id' in palette_header_decode_results:
+                palette_id = palette_header_decode_results['palette_id']
+                palette_name = palette_header_decode_results['palette_name']
+                palette_description = palette_header_decode_results['palette_description']
+                time_created = palette_header_decode_results['time_created']
+                number_of_colors = palette_header_decode_results['number_of_colors']
+                color_list = palette_header_decode_results['color_list']
+
+                results = import_custom_palette_from_header(palette_id, self.stream_read.stream_palette_id,
+                                                            palette_name, palette_description, time_created,
+                                                            number_of_colors, color_list)
+
+                # Failure
+                if results == False:
+                    self.frame_errors = self.ERROR_SOFT
+                    return
+
+                self.stream_palette = results['palette']
+                self.stream_read.stream_palette_load(self.stream_palette)
+                self.stream_palette_dict = self.stream_palette.return_decoder()
+                self.stream_palette_color_set = self.stream_palette.convert_colors_to_tuple()
+                self.scan_handler.set_stream_palette(self.stream_palette, self.stream_palette_dict,
+                                                     self.stream_palette_color_set)
+                self.stream_palette_loaded_this_frame = True
+
+        if self.frame_errors:
+            return
+        if self.frame_blocks_left and self.stream_read.custom_palette_used:
+
+            if self.stream_read.carry_over_header_bytes:
+                palette_header_bits = self.stream_read.return_pending_header_bits()
+                scan_length = (self.stream_read.palette_header_size_bytes * 8) - palette_header_bits.len
+                palette_scan_results = self.scan_handler.return_bits(scan_length, is_initializer_palette=True,
+                                                                     is_payload=True, byte_input=False)
+                palette_header_bits += palette_scan_results['bits']
+
+                if palette_scan_results['complete_request']:
+                    logging.debug('Custom palette header terminates this frame')
+                    self.palette_header_bytes = palette_header_bits.tobytes()
+                    self.frame_hashable_bits += palette_scan_results['bits']
+                    _palette_header_decode()
+
+                    self.stream_read.set_pending_header_bits()
+
+                else:
+                    logging.debug('Palette header rolling over to next frame...')
+                    self.stream_read.set_pending_header_bits(palette_header_bits)
+                    self.frame_blocks_left = False
+                    self.frame_hashable_bits += palette_scan_results['bits']
+                    return
+
+            else:
+                palette_scan_results = self.scan_handler.return_bits(self.stream_read.palette_header_size_bytes,
+                                                                     is_initializer_palette=True, is_payload=True,
+                                                                     byte_input=True)
+                palette_header_bits = palette_scan_results['bits']
+                if not palette_scan_results['complete_request']:
+                    logging.debug('Palette header rolling over to next frame...')
+                    self.stream_read.set_pending_header_bits(palette_header_bits)
+                    self.frame_blocks_left = False
+                    self.frame_hashable_bits += palette_header_bits
+                    return
+                self.palette_header_bytes = palette_header_bits.tobytes()
+                self.frame_hashable_bits += self.palette_header_bytes
+                _palette_header_decode()
+
+    def _payload_process(self):
+        if self.frame_errors:
+            return
+        if self.frame_blocks_left:
+            self.stream_payload_bits = self.scan_handler.return_payload_bits()['bits']
+            self.payload_in_frame = True
+
+    def _frame_validation(self):
+        if self.frame_errors:
+            return
+        if self.is_unique_frame:
+            frame_hashable_bytes = (self.frame_hashable_bits + self.stream_payload_bits).tobytes()
+            assert self.frame_hashable_bits.len + self.stream_payload_bits.len == self.bits_to_read
+            if self.frame_sha256 != get_sha256_hash_from_bytes(frame_hashable_bytes):
+                logging.warning('Frame corrupted.  Aborting frame...')
+                self.frame_errors = self.ERROR_SOFT
+                return
+
+            # Marking frame as complete, moving on to next frame
+            if self.payload_in_frame:
+                self.stream_frame.finalize_frame(self.stream_payload_bits)
+
+                # First frame with payload in it
+                if self.setup_headers_end_this_frame:
+                    self.stream_read.set_payload_start_data(self.frame_number, self.stream_payload_bits.len)
+            else:
+                self.stream_frame.finalize_frame()
+            self.stream_read.new_consecutive_frame(self.frame_number)
+
+    def _metadata_checkpoint(self):
+        if self.stream_read.stop_at_metadata_load and not self.stream_read.metadata_checkpoint_ran and \
+                self.stream_read.metadata_header_complete:
+            if self.stream_read.encrypted_metadata_header_bytes:
+                logging.info(f'Returning limited metadata from {self.stream_read}, as the encrypted metadata header'
+                             f' has not yet been decrypted.')
+            else:
+                logging.info(f'Returning metadata from {self.stream_read}')
+            self.metadata = self.stream_read.metadata_checkpoint_return()
+
+    def _run_statistics(self):
+        if self.save_statistics and self.is_unique_frame:
+            read_stats_update(self.scan_handler.block_position, 1, self.scan_handler.payload_bits_read)
diff --git a/bitglitter/read/process_state/multiprocess_state_generator.py b/bitglitter/read/process_state/multiprocess_state_generator.py
new file mode 100644
index 0000000..3393151
--- /dev/null
+++ b/bitglitter/read/process_state/multiprocess_state_generator.py
@@ -0,0 +1,40 @@
+import cv2
+
+from bitglitter.config.palettemodels import Palette
+
+from pathlib import Path
+
+
+def video_state_generator(video_frame_generator, stream_read, save_statistics, initializer_palette,
+                          initializer_palette_dict, initializer_color_set, total_video_frames, stream_palette=None,
+                          stream_palette_dict=None, stream_palette_color_set=None):
+    """Returns a dict object for frame_process to use when switching to multiprocessing"""
+
+    if not stream_palette:
+        stream_palette = Palette.query.filter(Palette.palette_id == stream_read.stream_palette_id).first()
+        stream_palette_dict = stream_palette.return_decoder()
+        stream_palette_color_set = stream_palette.convert_colors_to_tuple()
+
+    for returned_state in video_frame_generator:
+        yield {'mode': 'video', 'stream_read': stream_read, 'frame': returned_state['frame'], 'save_statistics':
+               save_statistics, 'initializer_palette_a': initializer_palette, 'initializer_palette_a_dict':
+               initializer_palette_dict, 'initializer_palette_a_color_set': initializer_color_set,
+               'current_frame_position': returned_state['current_frame_position'], 'total_frames': total_video_frames,
+               'stream_palette': stream_palette, 'stream_palette_dict': stream_palette_dict,
+               'stream_palette_color_set': stream_palette_color_set, 'sequential': False}
+
+
+def image_state_generator(input_list, initial_state_dict):
+    """Used for all image decoding regardless of placement, since they need to be approached with an empty slate in
+    terms of state.
+    """
+
+    total_frames = len(input_list)
+    count = 1
+    for image_path in input_list:
+        frame = cv2.imread(image_path)
+        path = Path(image_path)
+        file_name = path.name
+        yield {'mode': 'image', 'frame': frame, 'current_frame_position': count, 'total_frames': total_frames,
+               'starting_state': initial_state_dict, 'file_name': file_name}
+        count += 1
diff --git a/bitglitter/read/process_state/videoframegenerator.py b/bitglitter/read/process_state/videoframegenerator.py
new file mode 100644
index 0000000..aa2572a
--- /dev/null
+++ b/bitglitter/read/process_state/videoframegenerator.py
@@ -0,0 +1,15 @@
+import cv2
+
+
+def video_frame_generator(video_input_path):
+    """Opens the video file and yields one frame at a time for decoding"""
+
+    active_video = cv2.VideoCapture(video_input_path)
+    total_video_frames = int(active_video.get(cv2.CAP_PROP_FRAME_COUNT))
+    current_frame_position = 1
+
+    yield total_video_frames
+
+    for frame in range(total_video_frames):
+        yield {'frame': active_video.read()[1], 'current_frame_position': current_frame_position}
+        current_frame_position += 1
diff --git a/bitglitter/read/process_state/videoframeprocessor.py b/bitglitter/read/process_state/videoframeprocessor.py
new file mode 100644
index 0000000..13c4c32
--- /dev/null
+++ b/bitglitter/read/process_state/videoframeprocessor.py
@@ -0,0 +1,516 @@
+from bitstring import BitStream
+
+import logging
+
+from bitglitter.config.configfunctions import read_stats_update
+from bitglitter.config.palettefunctions import import_custom_palette_from_header
+from bitglitter.config.readmodels.streamread import StreamRead
+from bitglitter.config.readmodels.readmodels import StreamFrame, StreamSHA256Blacklist
+from bitglitter.read.decode.headerdecode import custom_palette_header_validate_decode, frame_header_decode, \
+    initializer_header_validate_decode, metadata_header_validate_decode, stream_header_decode
+from bitglitter.read.scan.scanvalidate import frame_lock_on, geometry_override_checkpoint
+from bitglitter.read.scan.scanhandler import ScanHandler
+from bitglitter.utilities.cryptography import get_sha256_hash_from_bytes
+
+
+class VideoFrameProcessor:
+    def __init__(self, dict_obj):
+        self.frame_position = dict_obj['current_frame_position']
+        total_frames = dict_obj['total_frames']
+        percentage_string = f'{round(((self.frame_position / total_frames) * 100), 2):.2f}'
+        logging.info(f"Processing video frame {self.frame_position} of {total_frames}... {percentage_string} %")
+
+        self.dict_obj = dict_obj
+
+        self.ERROR_SOFT = {'error': True}  # Only this frame is cancelled, decoding can continue on next frame
+        self.ERROR_FATAL = {'error': True, 'fatal': True}  # Entire session is cancelled
+        self.ERROR_BREAK = {'break': True}  # Not corruption related, some condition that requires skipping instead
+        self.frame_errors = {}
+        self.skip_process = False  # Stream has all frames, pending unpackaging
+
+        self.frame = self.dict_obj['frame']
+        self.frame_pixel_height = self.frame.shape[0]
+        self.frame_pixel_width = self.frame.shape[1]
+        self.initializer_palette_a = self.dict_obj['initializer_palette_a']
+        self.initializer_palette_a_color_set = self.dict_obj['initializer_palette_a_color_set']
+        self.initializer_palette_a_dict = self.dict_obj['initializer_palette_a_dict']
+
+        self.stream_palette = None
+        self.stream_palette_id = None
+        self.stream_palette_dict = None
+        self.stream_palette_color_set = None
+        self.stream_palette_loaded_this_frame = False
+
+        # State
+        self.is_sequential = self.dict_obj['sequential']
+        self.is_unique_frame = False
+        self.frame_hashable_bits = BitStream()
+        self.stream_payload_bits = BitStream()
+        self.frame_blocks_left = True
+        self.payload_in_frame = False
+        self.save_statistics = self.dict_obj['save_statistics']
+
+        self.stream_read = None
+        self.scan_handler = None
+        self.stream_frame = None
+        self.frame_sha256 = None
+        self.frame_number = None
+        self.metadata = None
+        self.metadata_header_bytes = None
+        self.palette_header_bytes = None
+        self.bits_to_read = None
+
+        # First frame variables
+        self.output_directory = None
+        self.block_height_override = None
+        self.block_width_override = None
+        self.decryption_key = None
+        self.scrypt_n = None
+        self.scrypt_r = None
+        self.scrypt_p = None
+        self.temp_save_directory = None
+        self.initializer_palette_b_color_set = None
+        self.initializer_palette_b_dict = None
+        self.auto_delete_finished_stream = None
+        self.auto_unpackage_stream = None
+        self.stop_at_metadata_load = None
+        self.block_height = None
+        self.block_width = None
+        self.pixel_width = None
+
+        # Video frames 2+
+        if 'stream_read' in self.dict_obj:
+            self.stream_read = self.dict_obj['stream_read']
+            self._second_frame_onwards_setup()
+            self._frame_header_process(first_frame=False)
+
+            # Still grabbing setup headers in this frame
+            if self.is_sequential and 'break' not in self.frame_errors:
+
+                if self.frame_number - 1 == self.stream_read.highest_consecutive_setup_frame_read:
+                    if not self.stream_read.metadata_header_complete:
+                        self._metadata_header_process_attempt()
+                    if not self.stream_read.palette_header_complete:
+                        self._palette_header_process_attempt()
+                    self._payload_process()
+                    self._frame_validation()
+                    self._metadata_checkpoint()
+
+                else:  # Treating as pure payload frame as a fallback.  This is a last resort.
+                    if self.stream_read.custom_palette_used and not self.stream_read.custom_palette_loaded:
+                        logging.warning('Cannot decode without custom palette loaded.  This error can occur if video'
+                                        ' frames are decoded out of order.  Aborting...')
+                        self.frame_errors = self.ERROR_FATAL
+                    else:
+                        self._payload_process()
+                        self._frame_validation()
+
+                if not self.stream_read.stream_header_complete:
+                    self._stream_header_process()
+
+            # Multiprocessing
+            else:
+                self._payload_process()
+                self._frame_validation()
+
+        # First video frame
+        else:
+            self._initial_frame_setup()
+            self._frame_header_process(first_frame=True)
+            self._stream_header_process()
+            self._metadata_header_process_attempt()
+            self._palette_header_process_attempt()
+            self._payload_process()
+            self._frame_validation()
+            self._metadata_checkpoint()
+
+        self._run_statistics()
+        self.scan_handler = None  # Keep as is, multiprocessing cannot return its internal generator and crashes
+        logging.debug('Frame decode cycle complete.')
+
+    def _initial_frame_setup(self):
+        self.output_directory = self.dict_obj['output_directory']
+        self.block_height_override = self.dict_obj['block_height_override']
+        self.block_width_override = self.dict_obj['block_width_override']
+        self.decryption_key = self.dict_obj['decryption_key']
+        self.scrypt_n = self.dict_obj['scrypt_n']
+        self.scrypt_r = self.dict_obj['scrypt_r']
+        self.scrypt_p = self.dict_obj['scrypt_p']
+        self.temp_save_directory = self.dict_obj['temp_save_directory']
+        self.initializer_palette_a = self.dict_obj['initializer_palette_a']
+        self.initializer_palette_a_color_set = self.dict_obj['initializer_palette_a_color_set']
+        self.initializer_palette_a_dict = self.dict_obj['initializer_palette_a_dict']
+        self.initializer_palette_b_color_set = self.dict_obj['initializer_palette_b_color_set']
+        self.initializer_palette_b_dict = self.dict_obj['initializer_palette_b_dict']
+        self.auto_delete_finished_stream = self.dict_obj['auto_delete_finished_stream']
+        self.auto_unpackage_stream = self.dict_obj['auto_unpackage_stream']
+        self.stop_at_metadata_load = self.dict_obj['stop_at_metadata_load']
+
+        # Geometry override checkpoint
+        if not geometry_override_checkpoint(self.block_height_override, self.block_width_override,
+                                            self.frame_pixel_height, self.frame_pixel_width):
+            self.frame_errors = self.ERROR_FATAL
+            return
+
+        # Frame lock on
+        lock_on_results = frame_lock_on(self.frame, self.block_height_override, self.block_width_override,
+                                        self.frame_pixel_height, self.frame_pixel_width,
+                                        self.initializer_palette_a_color_set, self.initializer_palette_b_color_set,
+                                        self.initializer_palette_a_dict, self.initializer_palette_b_dict)
+        if not lock_on_results:
+            self.frame_errors = self.ERROR_FATAL
+            return
+        self.block_height = lock_on_results['block_height']
+        self.block_width = lock_on_results['block_width']
+        self.pixel_width = lock_on_results['pixel_width']
+
+        # Now we passed minimum frame validation, moving to ScanHandler setup
+        self.scan_handler = ScanHandler(self.frame, True, self.initializer_palette_a, self.initializer_palette_a_dict,
+                                        self.initializer_palette_a_color_set)
+        self.scan_handler.set_scan_geometry(self.block_height, self.block_width, self.pixel_width)
+
+        #  Initializer scan and decode
+        initializer_results = self.scan_handler.return_initializer_bits()
+        if 'error' in initializer_results:
+            self.frame_errors = self.ERROR_FATAL
+            return
+        initializer_bits_raw = initializer_results['bits']
+        initializer_decode_results = initializer_header_validate_decode(initializer_bits_raw, self.block_height,
+                                                                        self.block_width)
+        if 'error' in initializer_decode_results:
+            self.frame_errors = self.ERROR_FATAL
+            return
+
+        protocol_version = initializer_decode_results['protocol_version']
+        custom_palette_used = initializer_decode_results['custom_palette_used']
+        self.stream_sha256 = initializer_decode_results['stream_sha256']
+
+        self.palette_header_complete = False
+        if initializer_decode_results['palette']:  # Palette already stored in db, not pending in future header
+            self.stream_palette = initializer_decode_results['palette']
+            self.stream_palette_dict = self.stream_palette.return_decoder()
+            self.stream_palette_color_set = self.stream_palette.convert_colors_to_tuple()
+            self.scan_handler.set_stream_palette(self.stream_palette, self.stream_palette_dict,
+                                                 self.stream_palette_color_set)
+            self.palette_header_complete = not custom_palette_used
+            self.stream_palette_loaded_this_frame = True
+            custom_palette_loaded = True
+        else:
+            self.stream_palette_id = initializer_decode_results['stream_palette_id']
+            custom_palette_loaded = False
+
+        # Blacklist check
+        blacklisted_hash = StreamSHA256Blacklist.query.filter(StreamSHA256Blacklist == self.stream_sha256).first()
+        if blacklisted_hash:
+            logging.warning(f'Hash {self.stream_sha256} is on your blacklist.  Aborting stream...')
+            self.frame_errors = self.ERROR_FATAL
+            return
+
+        # Loading or creating StreamRead instance
+        self.stream_read = StreamRead.query.filter(StreamRead.stream_sha256 == self.stream_sha256).first()
+        if self.stream_read:
+            if self.stream_read.is_complete:
+                logging.info(f'{self.stream_read.stream_name} -- {self.stream_sha256} is complete.  Aborting...')
+                self.frame_errors = self.ERROR_FATAL
+                return
+            else:
+                self.stream_read.session_activity(True, save=False)
+                self.stream_read.toggle_eligibility_calculations(True)
+            logging.info(f'Existing stream read found: {self.stream_read}')
+            if self.stream_read.all_frames_accounted_for:
+                logging.info('All frames accounted for, skipping video frame processing...')
+                self.skip_process = True
+                self.frame_errors = self.ERROR_BREAK
+
+        else:
+            logging.info(f'New stream: {self.stream_sha256}')
+            self.stream_read = StreamRead.create(stream_sha256=self.stream_sha256, stream_is_video=True,
+                                                 protocol_version=protocol_version, output_directory=
+                                                 self.output_directory, decryption_key=self.decryption_key, scrypt_n=
+                                                 self.scrypt_n, scrypt_r=self.scrypt_r, scrypt_p=self.scrypt_p,
+                                                 auto_delete_finished_stream=self.auto_delete_finished_stream,
+                                                 stop_at_metadata_load=self.stop_at_metadata_load,
+                                                 palette_header_complete=self.palette_header_complete,
+                                                 auto_unpackage_stream=self.auto_unpackage_stream, block_height=
+                                                 self.block_height, block_width=self.block_width, pixel_width=
+                                                 self.pixel_width, stream_palette_id=self.stream_palette_id,
+                                                 custom_palette_used=custom_palette_used, custom_palette_loaded=
+                                                 custom_palette_loaded)
+            if self.stream_palette:
+                self.stream_read.stream_palette_load(self.stream_palette)
+
+    def _frame_header_process(self, first_frame: bool):
+        if self.frame_errors:
+            return
+
+        if first_frame or self.stream_read.custom_palette_used and not self.stream_read.custom_palette_loaded:
+            is_initializer_palette = True
+        else:
+            is_initializer_palette = False
+
+        frame_header_bits_raw = self.scan_handler.return_frame_header_bits(is_initializer_palette=
+                                                                           is_initializer_palette)['bits']
+        frame_header_decode_results = frame_header_decode(frame_header_bits_raw)
+        if not frame_header_decode_results:
+            self.frame_errors = self.ERROR_FATAL
+            return
+        self.frame_sha256 = frame_header_decode_results['frame_sha256']
+        self.frame_number = frame_header_decode_results['frame_number']
+        self.bits_to_read = frame_header_decode_results['bits_to_read']
+        self.scan_handler.set_bits_to_read(self.bits_to_read)
+
+        # Checking if frame exists
+        self.stream_frame = StreamFrame.query.filter(StreamFrame.stream_id == self.stream_read.id) \
+            .filter(StreamFrame.frame_number == self.frame_number).first()
+        if self.stream_frame:  # Frame already loaded, skipping
+            if self.stream_frame.is_complete:  # Current frame is fully validated and saved
+                logging.info(f'Frame {self.frame_number} is already complete')
+            else:  # Current frame is being actively processed by another process
+                logging.debug(f'Pending active frame in another process: {self.frame_number}')
+            self.frame_blocks_left = False
+            self.frame_errors = self.ERROR_BREAK
+        else:  # New frame
+            self.stream_frame = StreamFrame.create(stream_id=self.stream_read.id, frame_number=self.frame_number)
+            logging.debug(f'New frame: #{self.frame_number}')
+            self.is_unique_frame = True
+
+    def _stream_header_process(self):
+        if self.frame_errors:
+            return
+        if self.frame_blocks_left:
+            stream_header_results = self.scan_handler.return_stream_header_bits(is_initializer_palette=True)
+
+            # Note- stream header always fits in first frame with current protocol, hence the missing carry over logic.
+            stream_header_bits_raw = stream_header_results['bits']
+            self.frame_hashable_bits += stream_header_results['bits']
+            stream_header_decode_results = stream_header_decode(stream_header_bits_raw)
+            if not stream_header_decode_results:
+                self.frame_errors = self.ERROR_FATAL
+                return
+            size_in_bytes = stream_header_decode_results['size_in_bytes']
+            total_frames = stream_header_decode_results['total_frames']
+            compression_enabled = stream_header_decode_results['compression_enabled']
+            encryption_enabled = stream_header_decode_results['encryption_enabled']
+            file_masking_enabled = stream_header_decode_results['file_masking_enabled']
+            metadata_header_length = stream_header_decode_results['metadata_header_length']
+            metadata_header_hash = stream_header_decode_results['metadata_header_hash']
+            custom_palette_header_length = stream_header_decode_results['custom_palette_header_length']
+            custom_palette_header_hash = stream_header_decode_results['custom_palette_header_hash']
+            self.stream_read.stream_header_load(size_in_bytes, total_frames, compression_enabled,
+                                                encryption_enabled, file_masking_enabled, metadata_header_length,
+                                                metadata_header_hash, custom_palette_header_length,
+                                                custom_palette_header_hash)
+
+    def _metadata_header_process_attempt(self):
+        def _metadata_header_decode():
+            """Actual header processing once all bytes are accounted for."""
+
+            metadata_header_decode_results = metadata_header_validate_decode(self.metadata_header_bytes,
+                                                                             self.stream_read.metadata_header_sha256_raw,
+                                                                             self.stream_read.decryption_key,
+                                                                             self.stream_read.encryption_enabled,
+                                                                             self.stream_read.file_masking_enabled,
+                                                                             self.stream_read.scrypt_n,
+                                                                             self.stream_read.scrypt_r,
+                                                                             self.stream_read.scrypt_p)
+
+            # Incorrect decryption key, storing header bytes in stream read
+            if metadata_header_decode_results == None:
+                self.stream_read.accept_encrypted_metadata_bytes(self.metadata_header_bytes)
+
+            # Corrupted header
+            elif metadata_header_decode_results == False:
+                self.frame_errors = self.ERROR_FATAL
+                return
+
+            # Successful and decode (and successful decryption if applicable)
+            elif 'bg_version' in metadata_header_decode_results:
+                bg_version = metadata_header_decode_results['bg_version']
+                stream_name = metadata_header_decode_results['stream_name']
+                stream_description = metadata_header_decode_results['stream_description']
+                time_created = metadata_header_decode_results['time_created']
+                manifest_string = metadata_header_decode_results['manifest_string']
+                self.stream_read.metadata_header_load(bg_version, stream_name, stream_description, time_created,
+                                                      manifest_string)
+        if self.frame_errors:
+            return
+        if self.frame_blocks_left:
+
+            if self.stream_read.carry_over_header_bytes:  # Loading in previous bytes from last frame to re-attempt
+                metadata_header_bits = self.stream_read.return_pending_header_bits()
+                scan_length = (self.stream_read.metadata_header_size_bytes * 8) - metadata_header_bits.len
+                metadata_scan_results = self.scan_handler.return_bits(scan_length, is_initializer_palette=True,
+                                                                      is_payload=True, byte_input=False)
+                metadata_header_bits += metadata_scan_results['bits']
+
+                if metadata_scan_results['complete_request']:  # Header terminates here
+                    logging.debug('Metadata header terminates this frame.')
+                    self.metadata_header_bytes = metadata_header_bits.tobytes()
+                    self.frame_hashable_bits += metadata_scan_results['bits']
+                    if self.stream_read.decryption_key and self.stream_read.file_masking_enabled or \
+                            not self.stream_read.file_masking_enabled:
+                        _metadata_header_decode()
+                        self.stream_read.set_pending_header_bits()
+                    else:
+                        logging.info('This stream has file masking enabled, and no decryption key was provided.  Cannot'
+                                     ' open metadata header until a decryption key (and a proper one) is provided!')
+                        self.stream_read.accept_encrypted_metadata_bytes(self.metadata_header_bytes)
+                        return
+                else:  # Header carries over into next frame
+                    logging.debug('Metadata header rolling over to next frame...')
+                    self.stream_read.set_pending_header_bits(metadata_header_bits)
+                    self.frame_blocks_left = False
+                    self.frame_hashable_bits += metadata_scan_results['bits']
+                    return
+
+            else:  # First frame
+                metadata_scan_results = self.scan_handler.return_bits(self.stream_read.metadata_header_size_bytes,
+                                                                      is_initializer_palette=True, is_payload=True,
+                                                                      byte_input=True)
+                metadata_header_bits = metadata_scan_results['bits']
+                if not metadata_scan_results['complete_request']:
+                    logging.debug('Metadata header rolling over to next frame...')
+                    self.stream_read.set_pending_header_bits(metadata_header_bits)
+                    self.frame_blocks_left = False
+                    self.frame_hashable_bits += metadata_header_bits
+                    return
+                self.metadata_header_bytes = metadata_header_bits.tobytes()
+                self.frame_hashable_bits += self.metadata_header_bytes
+                _metadata_header_decode()
+
+    def _palette_header_process_attempt(self):
+        def _palette_header_decode():
+            """Actual header processing once all bytes are accounted for."""
+            self.stream_read.palette_header_complete = True
+            self.stream_read.save()
+
+            if self.stream_read.custom_palette_loaded or not self.stream_read.custom_palette_used:
+                return
+
+            palette_header_decode_results = custom_palette_header_validate_decode(self.palette_header_bytes,
+                                                                                  self.stream_read.palette_header_sha256)
+
+            # Header integrity error
+            if palette_header_decode_results == False:
+                self.frame_errors = self.ERROR_FATAL
+                return
+
+            # Successfully decoded
+            elif 'palette_id' in palette_header_decode_results:
+                palette_id = palette_header_decode_results['palette_id']
+                palette_name = palette_header_decode_results['palette_name']
+                palette_description = palette_header_decode_results['palette_description']
+                time_created = palette_header_decode_results['time_created']
+                number_of_colors = palette_header_decode_results['number_of_colors']
+                color_list = palette_header_decode_results['color_list']
+
+                results = import_custom_palette_from_header(palette_id, self.stream_read.stream_palette_id,
+                                                            palette_name, palette_description, time_created,
+                                                            number_of_colors, color_list)
+
+                # Failure
+                if results == False:
+                    self.frame_errors = self.ERROR_FATAL
+                    return
+
+                self.stream_palette = results['palette']
+                self.stream_read.stream_palette_load(self.stream_palette)
+                self.stream_palette_dict = self.stream_palette.return_decoder()
+                self.stream_palette_color_set = self.stream_palette.convert_colors_to_tuple()
+                self.scan_handler.set_stream_palette(self.stream_palette, self.stream_palette_dict,
+                                                     self.stream_palette_color_set)
+                self.stream_palette_loaded_this_frame = True
+
+        if self.frame_errors:
+            return
+        if self.frame_blocks_left and self.stream_read.custom_palette_used:
+
+            if self.stream_read.carry_over_header_bytes:
+                palette_header_bits = self.stream_read.return_pending_header_bits()
+                scan_length = (self.stream_read.palette_header_size_bytes * 8) - palette_header_bits.len
+                palette_scan_results = self.scan_handler.return_bits(scan_length, is_initializer_palette=True,
+                                                                     is_payload=True, byte_input=False)
+                palette_header_bits += palette_scan_results['bits']
+
+                if palette_scan_results['complete_request']:
+                    logging.debug('Custom palette header terminates this frame')
+                    self.palette_header_bytes = palette_header_bits.tobytes()
+                    self.frame_hashable_bits += palette_scan_results['bits']
+                    _palette_header_decode()
+
+                    self.stream_read.set_pending_header_bits()
+
+                else:
+                    logging.debug('Palette header rolling over to next frame...')
+                    self.stream_read.set_pending_header_bits(palette_header_bits)
+                    self.frame_blocks_left = False
+                    self.frame_hashable_bits += palette_scan_results['bits']
+                    return
+
+            else:
+                palette_scan_results = self.scan_handler.return_bits(self.stream_read.palette_header_size_bytes,
+                                                                     is_initializer_palette=True, is_payload=True,
+                                                                     byte_input=True)
+                palette_header_bits = palette_scan_results['bits']
+                if not palette_scan_results['complete_request']:
+                    logging.debug('Palette header rolling over to next frame...')
+                    self.stream_read.set_pending_header_bits(palette_header_bits)
+                    self.frame_blocks_left = False
+                    self.frame_hashable_bits += palette_header_bits
+                    return
+                self.palette_header_bytes = palette_header_bits.tobytes()
+                self.frame_hashable_bits += self.palette_header_bytes
+                _palette_header_decode()
+
+
+    def _payload_process(self):
+        if self.frame_errors:
+            return
+        if self.frame_blocks_left:
+            self.stream_payload_bits = self.scan_handler.return_payload_bits()['bits']
+            self.payload_in_frame = True
+
+    def _frame_validation(self):
+        if self.frame_errors:
+            return
+        if self.is_unique_frame:
+            frame_hashable_bytes = (self.frame_hashable_bits + self.stream_payload_bits).tobytes()
+            assert self.frame_hashable_bits.len + self.stream_payload_bits.len == self.bits_to_read
+            if self.frame_sha256 != get_sha256_hash_from_bytes(frame_hashable_bytes):
+                logging.warning('Frame corrupted.  Aborting frame...')
+                self.frame_errors = self.ERROR_FATAL
+                return
+
+            # Marking frame as complete, moving on to next frame
+            if self.payload_in_frame:
+                self.stream_frame.finalize_frame(self.stream_payload_bits)
+                if self.is_sequential:  # First frame with payload in it
+                    self.stream_read.set_payload_start_data(self.frame_number, self.stream_payload_bits.len)
+            else:
+                self.stream_frame.finalize_frame()
+            if self.is_sequential:
+                self.stream_read.new_consecutive_frame(self.frame_number)
+
+    def _metadata_checkpoint(self):
+        if self.stream_read.stop_at_metadata_load and not self.stream_read.metadata_checkpoint_ran and \
+                self.stream_read.metadata_header_complete:
+            if self.stream_read.encrypted_metadata_header_bytes:
+                logging.info(f'Returning limited metadata from {self.stream_read}, as the encrypted metadata header'
+                             f' has not yet been decrypted.')
+            else:
+                logging.info(f'Returning metadata from {self.stream_read}')
+            self.metadata = self.stream_read.metadata_checkpoint_return()
+
+    def _run_statistics(self):
+        if self.save_statistics and self.is_unique_frame:
+            read_stats_update(self.scan_handler.block_position, 1, self.scan_handler.payload_bits_read)
+
+    def _second_frame_onwards_setup(self):
+        self.scan_handler = ScanHandler(self.frame, False, self.initializer_palette_a, self.initializer_palette_a_dict,
+                                        self.initializer_palette_a_color_set, block_height=
+                                        self.stream_read.block_height, block_width=self.stream_read.block_width,
+                                        pixel_width=self.stream_read.pixel_width)
+
+        if 'stream_palette' in self.dict_obj:
+            self.scan_handler.set_stream_palette(self.dict_obj['stream_palette'], self.dict_obj['stream_palette_dict'],
+                                                 self.dict_obj['stream_palette_color_set'])
diff --git a/bitglitter/read/read.py b/bitglitter/read/read.py
index 24cc487..070a313 100644
--- a/bitglitter/read/read.py
+++ b/bitglitter/read/read.py
@@ -1,53 +1,87 @@
-from bitglitter.config.constants import READ_PATH, BAD_FRAME_STRIKES, SCRYPT_N_DEFAULT, SCRYPT_R_DEFAULT, \
-    SCRYPT_P_DEFAULT
-from bitglitter.config.loggingset import logging_setter
-from bitglitter.read.verifyreadparameters import verify_read_parameters
+import logging
+from pathlib import Path
 
-def read(file_to_input,
-         output_path = None,
-         bad_frame_strikes = BAD_FRAME_STRIKES,
-         assemble_hold = False,
+from bitglitter.config.config import session
+from bitglitter.config.configmodels import Config, Constants
+from bitglitter.read.process_state.framereadhandler import frame_read_handler
+from bitglitter.utilities.filemanipulation import refresh_directory, remove_working_folder
+from bitglitter.utilities.loggingset import logging_setter
+from bitglitter.utilities.read import flush_inactive_frames
+from bitglitter.validation.validateread import validate_read_parameters
 
-         # Geometry Overrides
-         block_height_override = False,
-         block_width_override = False,
+
+def read(file_path,
+         stop_at_metadata_load=True,
+         auto_unpackage_stream=True,
+         auto_delete_finished_stream=True,
+         output_directory=None,
+         bad_frame_strikes=25,
+         max_cpu_cores=0,
+
+         # Overrides
+         block_height_override=False,
+         block_width_override=False,
 
          # Crypto Input
-         encryption_key = None,
-         scrypt_n = SCRYPT_N_DEFAULT,
-         scrypt_r = SCRYPT_R_DEFAULT,
-         scrypt_p = SCRYPT_P_DEFAULT,
+         decryption_key=None,
+         scrypt_n=14,
+         scrypt_r=8,
+         scrypt_p=1,
 
          # Logging Settings
-         logging_level ='info',
-         logging_screen_output = True,
-         logging_save_output = False
+         logging_level='info',
+         logging_screen_output=True,
+         logging_save_output=False,
+
+         # Session Data
+         save_statistics=False,
          ):
-    '''This is the high level function that decodes BitGlitter encoded images and video back into the files/folders
+    """This is the high level function that decodes BitGlitter encoded images and video back into the files/folders
     contained within them.  This along with write() are the two primary functions of this library.
-    '''
+    """
 
-    # Logging initializing.
-    logging_setter(logging_level, logging_screen_output, logging_save_output)
-    from bitglitter.read.fileslicer import file_slicer
-    from bitglitter.config.config import config
+    config = session.query(Config).first()
+    constants = session.query(Constants).first()
 
-    # Are all parameters acceptable?
-    verify_read_parameters(file_to_input, output_path, encryption_key, scrypt_n, scrypt_r, scrypt_p,
-                           block_width_override, block_width_override, assemble_hold)
+    valid_image_formats = constants.return_valid_image_formats(glob_format=True)
+
+    # Cleanup from previous session if crash:
+    flush_inactive_frames()
 
     # This sets the name of the temporary folder while screened data from partial saves is being written.
-    active_path = READ_PATH
+    working_directory = Path(constants.WORKING_DIR)
+    refresh_directory(working_directory)
+
+    # Setting save path for stream
+    if output_directory:
+        output_directory = output_directory
+    else:
+        output_directory = config.read_path
+        refresh_directory(config.read_path, delete=False)
+
+    # Logging initializing.
+    logging_setter(logging_level, logging_screen_output, logging_save_output, Path(config.log_txt_dir))
+    logging.info('Starting read...')
+
+    # Are all parameters acceptable?
+    input_type = validate_read_parameters(file_path, output_directory, decryption_key, scrypt_n, scrypt_r, scrypt_p,
+                                          block_height_override, block_width_override, max_cpu_cores, save_statistics,
+                                          bad_frame_strikes, stop_at_metadata_load, auto_unpackage_stream,
+                                          auto_delete_finished_stream)
 
     # Pull valid frame data from the inputted file.
-    checkpoint_passed = file_slicer(file_to_input, active_path, output_path, block_height_override,
-                                    block_width_override, encryption_key, scrypt_n, scrypt_r, scrypt_p, config,
-                                    bad_frame_strikes, assemble_hold)
-    if checkpoint_passed == False:
-        return False
-
-    # Now that all frames have been scanned, we'll have the config object check to see if any files are ready for
-    # assembly.  If there are, they will be put together and outputted, as well as removed/flushed from partialSaves.
-    config.assembler.review_active_sessions()
-    config.save_session()
-    return True
+    frame_read_results = frame_read_handler(file_path, output_directory, input_type, bad_frame_strikes, max_cpu_cores,
+                                            block_height_override, block_width_override, decryption_key, scrypt_n,
+                                            scrypt_r, scrypt_p, working_directory, stop_at_metadata_load,
+                                            auto_unpackage_stream, auto_delete_finished_stream, save_statistics,
+                                            valid_image_formats)
+
+    # Removing temporary directory
+    remove_working_folder(working_directory)
+
+    # Return metadata if conditions are met
+    if 'metadata' in frame_read_results:
+        return frame_read_results['metadata']
+
+    logging.info('Read cycle complete.')
+    return frame_read_results
diff --git a/bitglitter/read/savedfilefunctions.py b/bitglitter/read/savedfilefunctions.py
deleted file mode 100644
index fc40c13..0000000
--- a/bitglitter/read/savedfilefunctions.py
+++ /dev/null
@@ -1,78 +0,0 @@
-import os
-
-from bitglitter.config.config import config
-from bitglitter.utilities.generalverifyfunctions import is_int_over_zero, is_valid_directory, proper_string_syntax
-
-# These functions are for end users.
-
-def print_full_save_list(path, debug_data = False):
-    '''This function will output a text file displaying the current status of all of the partial saves in BitGlitter.
-    Path specifies the folder path it will be outputted to.  Argument debug_data will show various debug information
-    pertaining to the partial save, that the normal user has no need to see.
-    '''
-
-    active_path = os.path.join(os.getcwd(), path)
-
-    with open(active_path + '\\BitGlitter Partial Saves.txt', 'w') as writer:
-        writer.write('*' * 21 + '\nPartial Saves\n' + '*' * 21 + '\n')
-
-        if config.assembler.save_dict:
-            temp_holder = ''
-
-            for partial_save in config.assembler.save_dict:
-                temp_holder += ('\n' + config.assembler.save_dict[partial_save].return_status(debug_data) + '\n\n')
-            writer.write(temp_holder)
-
-        else:
-            writer.write('\nNo partial saves (yet!)')
-
-
-def update_partial_save(stream_sha, reattempt_assembly = True, password_update = None, scrypt_n =  None,
-                        scrypt_r = None, scrypt_p = None, change_output_path = None):
-    '''This function will update the PartialSave object with the parameters provided, once they've been verified.'''
-
-    if password_update:
-        proper_string_syntax('password_update', password_update)
-    if scrypt_n:
-        is_int_over_zero('scrypt_n', scrypt_n)
-    if scrypt_r:
-        is_int_over_zero('scrypt_r', scrypt_r)
-    if scrypt_p:
-        is_int_over_zero('scrypt_p', scrypt_p)
-    if change_output_path:
-        is_valid_directory('change_output_path', change_output_path)
-
-    config.assembler.save_dict[stream_sha].user_input_update(password_update, scrypt_n, scrypt_r, scrypt_p,
-                                                             change_output_path)
-
-    if reattempt_assembly == True:
-        if config.assembler.save_dict[stream_sha]._attempt_assembly() == True:
-            config.assembler.remove_partial_save(stream_sha)
-
-    config.save_session()
-
-
-def remove_partial_save(stream_sha):
-    '''Taking the stream SHA as an argument, this function will remove the partial save object, and well as remove any
-    temporary data BitGlitter may have had with it.
-    '''
-
-    config.assembler.remove_partial_save(stream_sha)
-    config.save_session()
-
-
-def begin_assembly(stream_sha):
-    '''This function exists to initiate assembly of a package at a later time, rather than doing so immediately for
-    whatever reason.
-    '''
-
-    if config.assembler.save_dict[stream_sha]._attempt_assembly() == True:
-        config.assembler.remove_partial_save(stream_sha)
-    config.save_session()
-
-
-def remove_all_partial_saves():
-    '''This removes all partial save objects saved, as well as any temporary data.'''
-
-    config.assembler.clear_partial_saves()
-    config.save_session()
diff --git a/bitglitter/read/scan/__init__.py b/bitglitter/read/scan/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/bitglitter/read/scan/scanhandler.py b/bitglitter/read/scan/scanhandler.py
new file mode 100644
index 0000000..4c172c5
--- /dev/null
+++ b/bitglitter/read/scan/scanhandler.py
@@ -0,0 +1,171 @@
+from bitstring import BitStream
+
+import math
+
+from bitglitter.read.scan.scanutilities import color_snap, scan_block
+
+
+class ScanHandler:
+    """This performs the low level scanning of the frame, and returns the raw bit/byte data for further validating and
+    processing.
+    """
+
+    def __init__(self, frame, has_initializer, initializer_palette, initializer_palette_dict, initializer_color_set,
+                 block_height=None, block_width=None, pixel_width=None, stream_palette=None, stream_palette_dict=None,
+                 stream_palette_color_set=None):
+        #  Core
+        self.frame = frame
+        self.has_initializer = has_initializer
+        self.block_height = block_height
+        self.block_width = block_width
+        self.pixel_width = pixel_width
+
+        #  Palette
+        self.initializer_palette = initializer_palette
+        self.initializer_palette_dict = initializer_palette_dict
+        self.initializer_color_set = initializer_color_set
+
+        self.stream_palette = stream_palette
+        self.stream_palette_dict = stream_palette_dict
+        self.stream_palette_color_set = stream_palette_color_set
+
+        #  Scan state
+        self.x_range = 0
+        self.y_range = 0
+        self.x_position = 0
+        self.y_position = 0
+        self.next_block_to_scan = None
+        self.block_position = 0
+        self.remaining_blocks = 0
+        self.bits_to_read = 0
+        self.payload_bits_read = 0
+        self.bits_read = 0
+        self.leftover_bits = BitStream()
+
+        # Class state before last action
+        self.prior_leftover_bits = BitStream()
+        self.prior_x_position = 0
+        self.prior_y_position = 0
+        self.prior_number_of_bits = 0 #?
+        self.prior_remaining_blocks = 0
+
+        #  Additional setup if we have more starting data
+        if self.block_height and self.block_width and self.pixel_width:
+            self._geometry_setup()
+            self.next_block_to_scan = self._frame_coordinate_generator()
+
+    def _geometry_setup(self):
+        self.x_range = self.block_width - int(self.has_initializer)
+        self.y_range = self.block_height - int(self.has_initializer)
+        self.remaining_blocks = self.x_range * self.y_range
+
+    def _undo_last_task(self):
+        """Moves coordinates in generator back before the last task was executed."""
+        self.leftover_bits = self.prior_leftover_bits
+        self.x_position = self.prior_x_position
+        self.y_position = self.prior_y_position
+        self.remaining_blocks = self.prior_remaining_blocks
+
+    def _frame_coordinate_generator(self):
+        """Creates a generator that outputs the correct block coordinates for scan_block to utilize.  This depends on
+         whether an initializer is used in this frame or not.
+        """
+
+        while self.y_position < self.y_range:
+            yield self.x_position + int(self.has_initializer), self.y_position + int(self.has_initializer)
+            self.x_position += 1
+            if self.x_position == self.x_range:
+                self.x_position = 0
+                self.y_position += 1
+
+    def return_bits(self, number_of_bits, is_initializer_palette, is_payload, byte_input=False, redo=False):
+
+        # Saving state before execution
+        if redo:
+            self._undo_last_task()
+        else:
+            self.prior_leftover_bits = self.leftover_bits
+            self.prior_x_position = self.x_position
+            self.prior_y_position = self.y_position
+            self.prior_remaining_blocks = self.remaining_blocks
+
+        if byte_input:
+            number_of_bits = number_of_bits * 8
+
+        if is_payload:
+            self.payload_bits_read += number_of_bits
+
+        if is_initializer_palette:
+            active_color_set = self.initializer_color_set
+            active_color_dict = self.initializer_palette_dict
+            active_bit_length = self.initializer_palette.bit_length
+        else:
+            active_color_set = self.stream_palette_color_set
+            active_color_dict = self.stream_palette_dict
+            active_bit_length = self.stream_palette.bit_length
+
+        complete_request = True
+        if number_of_bits:  # Set amount of bits to scan
+            number_of_blocks = int(math.ceil((number_of_bits - self.leftover_bits.len) / active_bit_length))
+            if number_of_blocks > self.remaining_blocks:
+                number_of_blocks = self.remaining_blocks
+                complete_request = False
+        else:  # If 0, scans the rest of the frame
+            number_of_blocks = self.remaining_blocks
+
+        bits = self.leftover_bits if self.leftover_bits.len > 0 else BitStream()
+
+        for block in range(number_of_blocks):
+            block_coordinates = next(self.next_block_to_scan)
+            average_rgb = scan_block(self.frame, self.pixel_width, block_coordinates[0], block_coordinates[1])
+
+            if active_color_set:  # Non-24 bit palette
+                bits.append(active_color_dict.get_value(color_snap(average_rgb, active_color_set)))
+            else:  # 24 bit palette
+                bits.append(active_color_dict.get_value(average_rgb))
+        self.remaining_blocks -= number_of_blocks
+
+        bits = BitStream(bits)
+        if bits.len > number_of_bits:
+            bits.pos = number_of_bits
+            self.leftover_bits = bits.read(bits.len - number_of_bits)
+            bits.pos = 0
+            bits = bits.read(number_of_bits)
+        if complete_request:
+            assert bits.len == number_of_bits
+
+        # Statistics update
+        self.block_position += number_of_blocks
+        self.bits_read += number_of_bits
+
+        return {'bits': bits, 'complete_request': complete_request}
+
+    def set_scan_geometry(self, block_height, block_width, pixel_width):
+        self.block_height = block_height
+        self.block_width = block_width
+        self.pixel_width = pixel_width
+        self._geometry_setup()
+        self.next_block_to_scan = self._frame_coordinate_generator()
+
+    def set_stream_palette(self, stream_palette, stream_palette_dict, stream_palette_color_set):
+        self.stream_palette = stream_palette
+        self.stream_palette_dict = stream_palette_dict
+        self.stream_palette_color_set = stream_palette_color_set
+
+    def set_bits_to_read(self, number_of_bits):
+        self.bits_to_read = number_of_bits
+
+    def return_initializer_bits(self):
+        """returns the initializer bitstring for the frame."""
+        return self.return_bits(580, True, is_payload=False)
+
+    def return_frame_header_bits(self, is_initializer_palette, redo=False):
+        return self.return_bits(352, is_initializer_palette, is_payload=False, redo=redo)
+
+    def return_stream_header_bits(self, is_initializer_palette):
+        return self.return_bits(685, is_initializer_palette, is_payload=True)
+
+    def return_payload_bits(self):
+        """Returns the remainder payload bits on the frame."""
+        remainder_bits = self.bits_to_read - self.payload_bits_read
+        return self.return_bits(remainder_bits, is_initializer_palette=False, is_payload=True)
diff --git a/bitglitter/read/scan/scanutilities.py b/bitglitter/read/scan/scanutilities.py
new file mode 100644
index 0000000..b97f826
--- /dev/null
+++ b/bitglitter/read/scan/scanutilities.py
@@ -0,0 +1,49 @@
+import numpy
+
+import math
+
+
+def return_distance(raw_frame_rgb, expected_value):
+    return math.sqrt(((raw_frame_rgb[0] - expected_value[0]) ** 2) + ((raw_frame_rgb[1] - expected_value[1]) ** 2) +
+                     ((raw_frame_rgb[2] - expected_value[2]) ** 2))
+
+
+def color_snap(raw_frame_rgb, palette_color_list):
+    """This takes the average RGB value of the pixels within the block, then loops through the full color palette to
+    determine the 'closest' palette.  These defined "snapped" values can then be used to convert the colors into bits.
+    """
+
+    closest_palette_match = None
+    closest_distance = 500
+    for color in palette_color_list:
+        active_distance = return_distance(raw_frame_rgb, color)
+        if active_distance < closest_distance:
+            closest_palette_match = color
+            closest_distance = active_distance
+    return closest_palette_match
+
+
+def scan_block(image, pixel_width, block_width_position, block_height_position):
+    """This function is whats used to scan the blocks used.  First the scan area is determined, and then each of the
+    pixels in that area appended to a list.  An average of those values as type int is returned.
+    """
+
+    if pixel_width < 5:
+        start_position_x = int(block_width_position * pixel_width)
+        end_position_x = int((block_width_position * pixel_width) + pixel_width - 1)
+        start_position_y = int(block_height_position * pixel_width)
+        end_position_y = int((block_height_position * pixel_width) + pixel_width - 1)
+
+    else:
+        start_position_x = int(round((block_width_position * pixel_width) + (pixel_width * .25), 1))
+        end_position_x = int(round(start_position_x + (pixel_width * .5), 1))
+        start_position_y = int(round((block_height_position * pixel_width) + (pixel_width * .25), 1))
+        end_position_y = int(round(start_position_y + (pixel_width * .5), 1))
+
+    numpy_output = numpy.flip(image[start_position_y:end_position_y, start_position_x:end_position_x]).mean(axis=(0, 1))
+    to_list_format = numpy_output.tolist()
+
+    for value in range(3):
+        to_list_format[value] = int(to_list_format[value])
+
+    return to_list_format
diff --git a/bitglitter/read/scan/scanvalidate.py b/bitglitter/read/scan/scanvalidate.py
new file mode 100644
index 0000000..2ac2de3
--- /dev/null
+++ b/bitglitter/read/scan/scanvalidate.py
@@ -0,0 +1,253 @@
+from bitstring import BitArray, ConstBitStream
+from numpy import flip
+
+import hashlib
+import logging
+
+from bitglitter.read.scan.scanutilities import color_snap, return_distance, scan_block
+
+
+def geometry_override_checkpoint(block_height_override, block_width_override, active_frame_size_height,
+                                 active_frame_size_width):
+    """If block_height_override and block_width_override have been entered, this checks those values against the height
+    and width (in pixels) of the image loaded).  Since the smallest blocks that can be read are one pixels (since that
+    is finest detail you can have with an image), any values beyond that are invalid, and stopped here.
+    """
+
+    if block_height_override and block_width_override:
+        if active_frame_size_width < block_width_override or active_frame_size_height < \
+                block_height_override:
+            logging.warning("Block override parameters are too large for a file of these dimensions.  "
+                            "Aborting...")
+            return False
+
+    return True
+
+
+def frame_lock_on(frame, block_height_override, block_width_override, frame_pixel_height, frame_pixel_width,
+                  initializer_palette_a_color_set, initializer_palette_b_color_set, initializer_palette_a_dict,
+                  initializer_palette_b_dict):
+    """This function is used to lock onto the frame.  If override values are present, these will be used.  Otherwise,
+    it will attempt to extract the correct values from the X and Y calibrator on the initial frame."""
+
+    logging.debug('Calibrator lock on...')
+    combined_colors = initializer_palette_a_color_set + initializer_palette_b_color_set
+
+    if block_height_override and block_width_override:  # Jump straight to validation
+        logging.info("block_height_override and block_width_override parameters detected.  Attempting to lock with "
+                     "these values...")
+        pixel_width = ((frame_pixel_width / block_width_override) + (frame_pixel_height / block_height_override)) / 2
+
+        checkpoint = verify_blocks_x(frame, pixel_width, block_width_override, combined_colors,
+                                     initializer_palette_a_dict, initializer_palette_b_dict, override=True)
+        if not checkpoint:
+            return False
+
+        checkpoint = verify_blocks_y(frame, pixel_width, block_height_override, combined_colors,
+                                     initializer_palette_a_dict, initializer_palette_b_dict, override=True)
+        if not checkpoint:
+            return False
+
+        block_width, block_height = block_width_override, block_height_override
+
+    else:
+        # First checkpoint.  Does pixel 0,0 have color_distance value of under 100 for black (0,0,0)?
+        if return_distance(frame[0, 0], (0, 0, 0)) > 100:
+            logging.warning('Frame lock fail!  Initial pixel value exceeds maximum color distance allowed for a '
+                            'reliable lock.')
+            return False
+
+        pixel_width, block_dimension_guess = pixel_creep(frame, initializer_palette_a_color_set,
+                                                         initializer_palette_b_color_set, combined_colors,
+                                                         initializer_palette_a_dict, initializer_palette_b_dict,
+                                                         frame_pixel_width, frame_pixel_height, width_axis=True)
+        if not pixel_width:
+            return False
+        checkpoint = verify_blocks_x(frame, pixel_width, block_dimension_guess, combined_colors,
+                                     initializer_palette_a_dict, initializer_palette_b_dict)
+        if not checkpoint:
+            return False
+
+        block_width = block_dimension_guess
+        pixel_width, block_dimension_guess = pixel_creep(frame, initializer_palette_a_color_set,
+                                                         initializer_palette_b_color_set, combined_colors,
+                                                         initializer_palette_a_dict, initializer_palette_b_dict,
+                                                         frame_pixel_width, frame_pixel_height, width_axis=False)
+        if not pixel_width:
+            return False
+        checkpoint = verify_blocks_y(frame, pixel_width, block_dimension_guess, combined_colors,
+                                     initializer_palette_a_dict, initializer_palette_b_dict)
+
+        if not checkpoint:
+            return False
+        block_height = block_dimension_guess
+
+    logging.debug(f'Lockon successful. \nPixel width: {pixel_width}\nBlock height: {block_height}\nBlock width: '
+                  f'{block_width}')
+    return {'block_height': block_height, 'block_width': block_width, 'pixel_width': pixel_width}
+
+
+def verify_blocks_x(image, pixel_width, block_width_estimate, combined_colors, initializer_palette_a_dict,
+                    initializer_palette_b_dict, override=False):
+    """This is a function used within frame_lock_on().  It verifies the correct values for the X axis."""
+
+    calibrator_bits_x = BitArray()
+    for x_block in range(17):
+        snapped_value = color_snap(scan_block(image, pixel_width, x_block, 0), combined_colors)
+
+        if x_block % 2 == 0:
+            calibrator_bits_x.append(initializer_palette_a_dict.get_value(snapped_value))
+
+        else:
+            calibrator_bits_x.append(initializer_palette_b_dict.get_value(snapped_value))
+
+    calibrator_bits_x.reverse()
+    read_calibrator_x = ConstBitStream(calibrator_bits_x)
+
+    if read_calibrator_x.read('uint:16') != block_width_estimate:
+        if override:
+            logging.warning('block_width_override is not equal to what was read on calibrator.  Aborting...')
+
+        else:
+            logging.warning('block_width validation does not match initial read.  This could be the result of \n'
+                            'sufficiently distorted frames.  Aborting...')
+
+        return False
+
+    if read_calibrator_x.read('bool'):
+        logging.warning('0,0 block unexpected value.  Aborting...')
+        return False
+
+    if override:
+        logging.info('block_width_override successfully verified.')
+
+    else:
+        logging.debug('block_width successfully verified.')
+
+    return True
+
+
+def verify_blocks_y(image, pixel_width, block_height_estimate, combined_colors, initializer_palette_a_dict,
+                    initializer_palette_b_dict, override=False):
+    """This is a function used within frame_lock_on().  It verifies the correct values for the Y axis."""
+
+    calibrator_bits_y = BitArray()
+    for y_block in range(17):
+        snapped_value = color_snap(scan_block(image, pixel_width, 0, y_block), combined_colors)
+
+        if y_block % 2 == 0:
+            calibrator_bits_y.append(initializer_palette_a_dict.get_value(snapped_value))
+
+        else:
+            calibrator_bits_y.append(initializer_palette_b_dict.get_value(snapped_value))
+
+    calibrator_bits_y.reverse()
+    read_calibrator_y = ConstBitStream(calibrator_bits_y)
+
+    if read_calibrator_y.read('uint:16') != block_height_estimate:
+        if override:
+            logging.warning('block_height_override is not equal to what was read on calibrator.  Aborting...')
+
+        else:
+            logging.warning('block_height validation does not match initial read.  This could be the result of \n'
+                            'sufficiently distorted frames.  Aborting...')
+
+        return False
+
+    if read_calibrator_y.read('bool'):
+        logging.warning('0,0 block unexpected value.  Aborting...')
+        return False
+
+    if override:
+        logging.info('block_height_override successfully verified.')
+
+    else:
+        logging.debug('block_height successfully verified.')
+
+    return True
+
+
+def pixel_creep(image, initializer_palette_a_color_set, initializer_palette_b_color_set, combined_colors,
+                initializer_palette_a_dict, initializer_palette_b_dict, image_width, image_height, width_axis):
+    """This function moves across the calibrator on the top and left of the frame one pixel at a time, and after
+    'snapping' the colors, decodes an unsigned integer from each axis, which if read correctly, is the block width and
+    block height of the frame.
+    """
+    calibrator_bits = BitArray()
+    snapped_values = []
+    active_color = (0, 0, 0)
+    pixel_on_dimension = 1
+    palette_a_is_active = False
+
+    axis_analyzed = image_width if width_axis else image_height
+    scan_limit = axis_analyzed // 2
+
+    for value in range(16):
+        while True:
+            axis_on_image = (0, pixel_on_dimension) if width_axis else (pixel_on_dimension, 0)
+            new_palette_locked = False
+            active_scan = flip(image[axis_on_image])
+            active_distance = return_distance(active_scan, active_color)
+
+            pixel_on_dimension += 1
+            if pixel_on_dimension >= scan_limit:
+                return 0, 0
+            if active_distance < 100:  # Iterating over same colored blocks, until distance exceeds 100.
+                continue
+
+            else:  # We are determining if we are within < 100 dist of a new color, or are in fuzzy space.
+                if not palette_a_is_active:
+                    active_palette = initializer_palette_b_color_set
+
+                else:
+                    active_palette = initializer_palette_a_color_set
+
+                for color in active_palette:
+                    active_distance = return_distance(active_scan, color)
+
+                    if active_distance < 100:
+                        palette_a_is_active = not palette_a_is_active
+                        new_palette_locked = True
+                        break
+
+                    else:
+                        continue
+
+            if new_palette_locked:
+                break
+
+        active_color = color_snap(active_scan, combined_colors)
+        snapped_values.append(active_color)
+
+        if value % 2 != 0:
+            calibrator_bits.append(initializer_palette_a_dict.get_value(active_color))
+
+        else:
+            calibrator_bits.append(initializer_palette_b_dict.get_value(active_color))
+
+    calibrator_bits.reverse()
+    read_calibrator_bits = ConstBitStream(calibrator_bits)
+    block_dimension_guess = read_calibrator_bits.read('uint:16')
+    pixel_width = axis_analyzed / block_dimension_guess
+
+    return pixel_width, block_dimension_guess
+
+
+def validate_payload(payload_bits, read_frame_sha):
+    """Taking all of the frame bits after the frame header, this takes the SHA-256 hash of them, and compares it against
+    the frame SHA written in the frame header.  This is the primary mechanism that validates frame data, which either
+    allows it to be passed through to the assembler, or discarded.
+    """
+
+    sha_hasher = hashlib.sha256()
+    sha_hasher.update(payload_bits.tobytes())
+    string_output = sha_hasher.hexdigest()
+    logging.debug(f'length of payload_bits: {payload_bits.len}')
+
+    if string_output != read_frame_sha:
+        logging.warning('validate_payload: read_frame_sha does not match calculated one.  Aborting...')
+        logging.debug(f'Read from frameHeader: {read_frame_sha}\nCalculated just now: {string_output}')
+        return False
+
+    logging.debug('Payload validated this frame.')
+    return True
diff --git a/bitglitter/read/verifyreadparameters.py b/bitglitter/read/verifyreadparameters.py
deleted file mode 100644
index e1ae3b7..0000000
--- a/bitglitter/read/verifyreadparameters.py
+++ /dev/null
@@ -1,39 +0,0 @@
-import logging
-import os
-
-from bitglitter.config.constants import VALID_IMAGE_FORMATS, VALID_VIDEO_FORMATS
-from bitglitter.utilities.generalverifyfunctions import is_valid_directory, is_int_over_zero, proper_string_syntax, \
-    is_bool
-
-
-def verify_read_parameters(file_to_input, output_path, encryption_key, scrypt_n, scrypt_r, scrypt_p,
-                           block_height_override, block_width_override, assemble_hold):
-    '''This function verifies the arguments going into read() to ensure they comform with the required format for
-    processing.
-    '''
-
-    logging.info("Verifying read parameters...")
-
-    if not os.path.isfile(file_to_input):
-        raise ValueError(f'file_to_input argument {file_to_input} must be a file.')
-
-    if os.path.splitext(file_to_input)[1] not in VALID_VIDEO_FORMATS and \
-            os.path.splitext(file_to_input)[1] not in VALID_IMAGE_FORMATS:
-        raise ValueError(f'file_to_input argument {file_to_input} is not a valid format.  Only the following are '
-                         f'allowed: {VALID_IMAGE_FORMATS}, and {VALID_VIDEO_FORMATS}')
-
-    if output_path:
-        is_valid_directory('file_to_input', output_path)
-
-    proper_string_syntax('encryption_key', encryption_key)
-
-    is_int_over_zero('scryptOverrideN', scrypt_n)
-    is_int_over_zero('scryptOverrideR', scrypt_r)
-    is_int_over_zero('scryptOverrideP', scrypt_p)
-
-    is_int_over_zero('block_height_override', block_height_override)
-    is_int_over_zero('block_width_override', block_width_override)
-
-    is_bool('assemble_hold', assemble_hold)
-
-    logging.info("All read parameters within acceptable range.")
\ No newline at end of file
diff --git a/bitglitter/read/videoframepuller.py b/bitglitter/read/videoframepuller.py
deleted file mode 100644
index 8f53387..0000000
--- a/bitglitter/read/videoframepuller.py
+++ /dev/null
@@ -1,22 +0,0 @@
-import logging
-
-import cv2
-
-
-class VideoFramePuller:
-    '''This object is loads the BitGlitter-encoded video, and returns one frame at a time to be decoded.'''
-
-    def __init__(self, file_to_input):
-
-        self.active_video = cv2.VideoCapture(file_to_input)
-        logging.debug('Video successfully loaded into VideoFramePuller.')
-        self.total_frames = int(self.active_video.get(cv2.CAP_PROP_FRAME_COUNT))
-        logging.info(f'{self.total_frames} frame(s) detected in video.')
-        self.current_frame = 1
-
-
-    def next_frame(self):
-        '''This method will automatically read the next frame from the video, and return the file path to that image.'''
-
-        self.current_frame += 1
-        return self.active_video.read()[1]
\ No newline at end of file
diff --git a/bitglitter/utilities/__init__.py b/bitglitter/utilities/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/bitglitter/utilities/compression.py b/bitglitter/utilities/compression.py
new file mode 100644
index 0000000..3a93f3c
--- /dev/null
+++ b/bitglitter/utilities/compression.py
@@ -0,0 +1,58 @@
+import os
+import zlib
+
+
+def compress_file(input_file, output_file, write_mode, remove_input=True):
+    """This inputs a file, and writes a compressed one, removing the input file afterwards by default."""
+    if write_mode == 'write':
+        mode = 'wb'
+    elif write_mode == 'append':
+        mode = 'ab'
+    else:
+        raise ValueError('\'write\' and \'append\' are the only allowed strings for write_mode.')
+
+    compressor = zlib.compressobj(9)
+    with open(input_file, 'rb') as decompressed:
+        with open(output_file, mode) as compressed:
+            chunk_size = 1000000
+            while True:
+                chunk = decompressed.read(chunk_size)
+                if chunk:
+                    compressed.write(compressor.compress(chunk))
+                else:
+                    compressed.write(compressor.flush())
+                    break
+
+    if remove_input:
+        os.remove(input_file)
+
+
+def compress_bytes(input_bytes):
+    compressed = zlib.compress(input_bytes, level=9)
+    return compressed
+
+
+def decompress_file(input_file, output_file, remove_input=True):
+    """Doing the opposite as compress_file(), this inputs a compressed file, and writes a decompressed one, while
+    removing the original file by default.
+    """
+
+    decompressor = zlib.decompressobj()
+    with open(input_file, 'rb') as compressed:
+        with open(output_file, 'wb') as decompressed:
+            chunk_size = 1000000
+            while True:
+                chunk = compressed.read(chunk_size)
+                if chunk:
+                    decompressed.write(decompressor.decompress(chunk))
+                else:
+                    decompressed.write(decompressor.flush())
+                    break
+
+    if remove_input:
+        os.remove(input_file)
+
+
+def decompress_bytes(input_bytes):
+    decompressed = zlib.decompress(input_bytes)
+    return decompressed
diff --git a/bitglitter/utilities/cryptography.py b/bitglitter/utilities/cryptography.py
new file mode 100644
index 0000000..ce719c6
--- /dev/null
+++ b/bitglitter/utilities/cryptography.py
@@ -0,0 +1,159 @@
+from cryptography.hazmat.primitives.ciphers import Cipher, modes
+from cryptography.hazmat.primitives.ciphers.algorithms import AES
+from cryptography.hazmat.primitives.padding import PKCS7
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
+
+import hashlib
+from io import BytesIO
+import os
+
+
+def encrypt_file(input_file, output_file, write_mode, encryption_key, scrypt_n=14, scrypt_r=8, scrypt_p=1,
+                 remove_input=True):
+    """Taking an input file as well as the ASCII encryption key, the file is encrypted with AES-256, and outputted to
+    output_file.  The input file is removed by default.
+    """
+    if write_mode == 'write':
+        mode = 'wb'
+    elif write_mode == 'append':
+        mode = 'ab'
+    else:
+        raise ValueError('\'write\' and \'append\' are the only allowed strings for write_mode.')
+
+    backend = default_backend()
+    initialization_vector = os.urandom(AES.block_size // 8)
+    salt = os.urandom(AES.block_size // 8)
+    key = _derive_key(encryption_key, salt, scrypt_n, scrypt_r, scrypt_p, backend)
+
+    with open(output_file, mode) as encrypted:
+        with open(input_file, 'rb') as decrypted:
+            encrypted.write(initialization_vector)
+            encrypted.write(salt)
+
+            chunk_size = 1048576
+            while True:
+                chunk = decrypted.read(chunk_size)
+                if chunk:
+                    encrypted.write(_encrypt_bytes_chunk(key, initialization_vector, chunk, backend))
+                else:
+                    break
+
+    if remove_input:
+        os.remove(input_file)
+
+
+def encrypt_bytes(input_bytes, encryption_key, scrypt_n=14, scrypt_r=8, scrypt_p=1):
+    backend = default_backend()
+    initialization_vector_aes = os.urandom(AES.block_size // 8)
+    salt = os.urandom(AES.block_size // 8)
+
+    encrypted_bytes = b''
+    encrypted_bytes += initialization_vector_aes
+    encrypted_bytes += salt
+    encrypted_bytes += _encrypt_bytes_chunk(_derive_key(encryption_key, salt, scrypt_n, scrypt_r, scrypt_p,
+                                                        backend), initialization_vector_aes, input_bytes, backend)
+
+    return encrypted_bytes
+
+
+def decrypt_file(input_file, output_file, encryption_key, scrypt_n=14, scrypt_r=8, scrypt_p=1,
+                 remove_input=True):
+    """Taking an input file as well as an encryption key, this function decrypts and saves the file."""
+
+    backend = default_backend()
+    with open(output_file, 'wb') as decrypted:
+        with open(input_file, 'rb') as encrypted:
+            initialization_vector = encrypted.read(AES.block_size // 8)
+            salt = encrypted.read(AES.block_size // 8)
+            key = _derive_key(encryption_key, salt, scrypt_n, scrypt_r, scrypt_p, backend)
+            chunk_size = 1048576
+            while True:
+                chunk = encrypted.read(chunk_size)
+                if chunk:
+                    decrypted.write(_decrypt_bytes_chunk(key, initialization_vector, chunk, backend,
+                                                         chunk_size=chunk_size))
+                else:
+                    break
+
+    if remove_input:
+        os.remove(input_file)
+
+
+def decrypt_bytes(input_bytes, encryption_key, scrypt_n=14, scrypt_r=8, scrypt_p=1):
+    backend = default_backend()
+    returned_bytes = b''
+    with BytesIO(input_bytes) as encrypted:
+        initialization_vector = encrypted.read(AES.block_size // 8)
+        salt = encrypted.read(AES.block_size // 8)
+        key = _derive_key(encryption_key, salt, scrypt_n, scrypt_r, scrypt_p, backend)
+        chunk_size = 1048576
+        try:
+            while True:
+                chunk = encrypted.read(chunk_size)
+                if chunk:
+                    returned_bytes += (_decrypt_bytes_chunk(key, initialization_vector, chunk, backend,
+                                                         chunk_size=chunk_size))
+                else:
+                    break
+        #  Incorrect decryption values
+        except ValueError:
+            return None
+    return returned_bytes
+
+def _encrypt_bytes_chunk(key, initialization_vector, data, backend):
+    """This is an internal function used in encrypt_file(), and for future functionality of this program.  It returns
+    32 bytes of encrypted data.
+    """
+
+    cipher = Cipher(AES(key), modes.CBC(initialization_vector), backend=backend)
+    padder = PKCS7(AES.block_size).padder()
+    encryptor = cipher.encryptor()
+    return encryptor.update(padder.update(data) + padder.finalize()) + encryptor.finalize()
+
+
+def _decrypt_bytes_chunk(key, initialization_vector_aes, data, backend, chunk_size=None):
+    """This is an internal function used in decrypt_file(), and for future functionality of this program.  It returns
+    32 bytes of decrypted data.
+    """
+
+    cipher = Cipher(AES(key), modes.CBC(initialization_vector_aes), backend=backend)
+    unpadder = PKCS7(AES.block_size).unpadder()
+    decryptor = cipher.decryptor()
+    if len(data) == chunk_size:
+        return unpadder.update(decryptor.update(data) + decryptor.finalize())
+    else:
+        return unpadder.update(decryptor.update(data) + decryptor.finalize()) + unpadder.finalize()
+
+
+def _derive_key(password, salt, scrypt_n, scrypt_r, scrypt_p, backend):
+    """This is an internal function used in encrypt_file() and decrypt_file() that creates the text based key, and
+    returns a proper AES key in byte format.
+    """
+
+    kdf = Scrypt(salt=salt, length=32, n=2 ** scrypt_n, r=scrypt_r, p=scrypt_p,
+                 backend=backend)
+    return kdf.derive(password.encode())
+
+
+def get_hash_from_file(file_path, byte_output=False):
+    """Taking in a path to a file as an argument, it returns the SHA-256 hash of the file via a string."""
+    sha256 = hashlib.sha256()
+    with open(file_path, 'rb') as file_to_hash:
+        buffer = file_to_hash.read(100000)
+        while len(buffer) > 0:
+            sha256.update(buffer)
+            buffer = file_to_hash.read(100000)
+    if byte_output:
+        return sha256.digest()
+    else:
+        return sha256.hexdigest()
+
+
+def get_sha256_hash_from_bytes(input_bytes, byte_output=False):
+    sha256 = hashlib.sha256()
+    sha256.update(input_bytes)
+    if byte_output:
+        return sha256.digest()
+    else:
+        return sha256.hexdigest()
diff --git a/bitglitter/utilities/display.py b/bitglitter/utilities/display.py
new file mode 100644
index 0000000..c62abba
--- /dev/null
+++ b/bitglitter/utilities/display.py
@@ -0,0 +1,13 @@
+def humanize_file_size(size_in_bytes):
+    if 0 < size_in_bytes < 1000:
+        return f'{size_in_bytes} B'
+    elif 1000 <= size_in_bytes < 1000 ** 2:
+        return f'{round((size_in_bytes / 1000), 2)} KB'
+    elif 1000 ** 2 <= size_in_bytes < 1000 ** 3:
+        return f'{round((size_in_bytes / 1000 ** 2), 2)} MB'
+    elif 1000 ** 3 <= size_in_bytes < 1000 ** 4:
+        return f'{round((size_in_bytes / 1000 ** 3), 2)} GB'
+    elif 1000 ** 4 <= size_in_bytes < 1000 ** 5:
+        return f'{round((size_in_bytes / 1000 ** 4), 2)} TB'
+    elif 1000 ** 5 <= size_in_bytes < 1000 ** 6:
+        return f'{round((size_in_bytes / 1000 ** 5), 2)} PB'
diff --git a/bitglitter/utilities/filemanipulation.py b/bitglitter/utilities/filemanipulation.py
index 779f9a5..a43c137 100644
--- a/bitglitter/utilities/filemanipulation.py
+++ b/bitglitter/utilities/filemanipulation.py
@@ -1,148 +1,38 @@
-import hashlib
-import gzip
 import logging
 import os
 import shutil
 
-from cryptography.hazmat.primitives.ciphers import Cipher, modes
-from cryptography.hazmat.primitives.ciphers.algorithms import AES
-from cryptography.hazmat.primitives.padding import PKCS7
-from cryptography.hazmat.backends import default_backend
-from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
 
-def compress_file(input_file, output_file, remove_input = True):
-    '''This inputs a file, and writes a compressed one, removing the input file afterwards by default.'''
+def refresh_directory(active_path, delete=True):
+    """Used for working (temp) directory, and default directory for read/write.  Optionally removes directory if it
+    exists, otherwise it makes sure it is created.
+    """
+    active_folder = os.path.join(os.getcwd(), active_path)
 
-    with open(input_file, 'rb') as process_in:
-        with open(output_file, 'wb') as process_out:
-            with gzip.GzipFile(None, 'wb', fileobj=process_out, mtime=0.) as process_out:
-                shutil.copyfileobj(process_in, process_out)
+    if delete:
+        if os.path.isdir(active_path):
+            shutil.rmtree(active_path)
 
-    if remove_input == True:
-        os.remove(input_file)
+    if not os.path.isdir(active_path):
+        os.makedirs(active_path)
+    return active_folder
 
 
-def decompress_file(input_file, output_file, remove_input = True):
-    '''Doing the opposite as compress_file(), this inputs a compressed file, and writes a decompressed one, while
-    removing the original file by default.
-    '''
+def remove_working_folder(working_directory):
+    if os.path.isdir(working_directory):
+        logging.debug('Deleting temporary working directory...')
+        shutil.rmtree(working_directory)
 
-    with gzip.open(input_file, 'rb') as process_in:
-        with open(output_file, 'wb') as process_out:
-            shutil.copyfileobj(process_in, process_out)
 
-    if remove_input == True:
-        os.remove(input_file)
+def create_default_output_folder(default_output_path):
+    if not os.path.isdir(default_output_path):
+        logging.debug('Default output directory does not exist.  Creating...')
+        os.mkdir(default_output_path)
 
 
-def encrypt_file(input_file, output_file, encryption_key, scrypt_n = 14, scrypt_r = 8, scrypt_p = 1,
-                 remove_input = True):
-    '''Taking an input file as well as the ASCII encryption key, the file is encrypted with AES-256, and outputted to
-    output_file.  The input file is removed by default.
-    '''
+def return_file_size(file_path):
+    """This is taking the final size of the pre-processed file, and this number will be used in the rendering process.
+    """
 
-    backend = default_backend()
-    initialization_vector_aes = os.urandom(AES.block_size // 8)
-    salt = os.urandom(AES.block_size // 8)
-
-    with open(output_file, 'wb') as file_out:
-        with open(input_file, 'rb') as file_in:
-            file_out.write(initialization_vector_aes)
-            file_out.write(salt)
-            file_out.write(_encrypt_bytes(_derive_key(encryption_key, salt, scrypt_n, scrypt_r, scrypt_p, backend),
-                                          initialization_vector_aes, file_in.read(), backend))
-
-    if remove_input == True:
-        os.remove(input_file)
-
-
-def decrypt_file(input_file, output_file, encryption_key, scrypt_n = 14, scrypt_r = 8, scrypt_p = 1,
-                 remove_input = True):
-    '''Taking an input file as well as an encryption key, this function decrypts and saves the file.'''
-
-    backend = default_backend()
-    with open(output_file, 'wb') as file_out:
-        with open(input_file, 'rb') as file_in:
-            iv = file_in.read(AES.block_size // 8)
-            salt = file_in.read(AES.block_size // 8)
-            file_out.write(_decrypt_bytes(_derive_key(encryption_key, salt, scrypt_n, scrypt_r, scrypt_p, backend), iv,
-                                          file_in.read(), backend))
-
-    if remove_input == True:
-        os.remove(input_file)
-
-
-def _encrypt_bytes(key, initialization_vector, data, backend):
-    '''This is an internal function used in encrypt_file(), and for future functionality of this program.  It returns
-    32 bytes of encrypted data.
-    '''
-
-    cipher = Cipher(AES(key), modes.CBC(initialization_vector), backend=backend)
-    padder = PKCS7(AES.block_size).padder()
-    encryptor = cipher.encryptor()
-    return encryptor.update(padder.update(data) + padder.finalize()) + encryptor.finalize()
-
-
-def _decrypt_bytes(key, initialization_vector_aes, data, backend):
-    '''This is an internal function used in dercryptFile(), and for future functionality of this program.  It returns
-    32 bytes of decrypted data.
-    '''
-
-    cipher = Cipher(AES(key), modes.CBC(initialization_vector_aes), backend=backend)
-    unpadder = PKCS7(AES.block_size).unpadder()
-    decryptor = cipher.decryptor()
-    return unpadder.update(decryptor.update(data) + decryptor.finalize()) + unpadder.finalize()
-
-
-def _derive_key(password, salt, scrypt_n, scrypt_r, scrypt_p, backend):
-    '''This is an internal function used in encrypt_file() and decrypt_file() that creates the text based key, and
-    returns a proper AES key in byte format.
-    '''
-
-    kdf = Scrypt(salt=salt, length=32, n=2 ** scrypt_n, r=scrypt_r, p=scrypt_p,
-                 backend=backend)
-    return kdf.derive(password.encode())
-
-
-def return_hash_from_file(pass_through):
-    '''Taking in a path to a file as an argument, it returns the SHA-256 hash of the file via a string.'''
-
-    sha256 = hashlib.sha256()
-    with open(pass_through, 'rb') as file_to_hash:
-        buffer = file_to_hash.read(100000)
-        while len(buffer) > 0:
-            sha256.update(buffer)
-            buffer = file_to_hash.read(100000)
-    return sha256.hexdigest()
-
-
-def refresh_working_folder(active_path):
-    '''While the temporary folder after write gets automatically deleted, things such as abrupt stops can prevent that
-    from happening.  This removes the folder if it is present, and then creates a few one for the next write cycle.
-    This runs in the beginning of preProcess.
-    '''
-    activeFolder = os.path.join(os.getcwd(), active_path)
-
-    if os.path.isdir(active_path):
-        shutil.rmtree(active_path)
-        logging.debug(f"active_path folder '{active_path}' already exists.  Deleting...")
-
-    os.mkdir(active_path)
-    logging.debug(f"Temp folder '{active_path}' created.")
-    return activeFolder
-
-
-def return_file_size(pass_through):
-    '''This is taking the final size of the pre-processed file, and this number will be used in the rendering process.
-    '''
-
-    size = os.path.getsize(pass_through)
+    size = os.path.getsize(file_path)
     return size
-
-
-def format_file_list(file_string):
-    '''This takes in the file manifest inside of the stream header, and prints it in a nice formatted way.'''
-
-    broken_apart = file_string.split('|')[1:]
-    for position in range(int(len(broken_apart) / 2)):
-        print(f"    {broken_apart[2 * position]} - {broken_apart[2 * position + 1]} B")
\ No newline at end of file
diff --git a/bitglitter/utilities/generalverifyfunctions.py b/bitglitter/utilities/generalverifyfunctions.py
deleted file mode 100644
index c1957d5..0000000
--- a/bitglitter/utilities/generalverifyfunctions.py
+++ /dev/null
@@ -1,33 +0,0 @@
-import os
-import string
-
-
-def is_valid_directory(argument, path):
-    '''Checks whether the inputted directory exists or not.'''
-    if not os.path.isdir(path):
-        raise ValueError(f'Folder path for {argument} does not exist.')
-
-
-def is_int_over_zero(argument, some_variable):
-    '''Checks if variable is of type int and is greater than zero.'''
-
-    if not isinstance(some_variable, int) or some_variable < 0:
-        raise ValueError(f"Argument {argument} must be an integer greater than zero.")
-
-
-def proper_string_syntax(argument, inputted_string=""):
-    '''This exists to verify various inputs are using allowed characters.  One such character that isn't allowed is
-    '\\', as that character is what the ASCII part of the stream header uses to divide attributes.
-    '''
-
-    acceptable_chars = string.ascii_letters + string.digits + string.punctuation.replace("\\", "") + " "
-    if inputted_string:
-        for letter in inputted_string:
-            if letter not in acceptable_chars:
-                raise ValueError(f"Only ASCII characters excluding '\\' are allowed in {argument}.")
-
-
-def is_bool(argument, variable):
-    '''Will raise error if argument isn't type bool.  Used in verifying arguments for read() and write()'''
-    if not isinstance(variable, bool):
-        raise ValueError(f'Argument {argument} must be type boolean.')
\ No newline at end of file
diff --git a/bitglitter/utilities/loggingset.py b/bitglitter/utilities/loggingset.py
new file mode 100644
index 0000000..80fb9ec
--- /dev/null
+++ b/bitglitter/utilities/loggingset.py
@@ -0,0 +1,35 @@
+import datetime
+import logging
+import os
+
+from bitglitter.validation.utilities import logging_config_validate
+
+
+def logging_setter(logging_level, logging_stdout_output, logging_txt_output, logging_save_path=None):
+    """This is what the logging level and output during the write operation.  It's worth nothing that this is the ONLY
+    function that checks parameters outside of verify_write_parameters, because that function's logging outputs hinge in
+    the configuration for logging.
+    """
+
+    logging_config_validate(logging_level, logging_stdout_output, logging_txt_output)
+    logging_level_dict = {None: None, False: None, 'debug': logging.DEBUG, 'info': logging.INFO}
+    if logging_txt_output:
+        if not os.path.isdir(logging_save_path):
+            os.makedirs(logging_save_path)
+        log_output_name = f"logs\\{datetime.datetime.now().strftime('%Y-%m-%d %H-%M-%S')}.txt"
+        output_log = logging.getLogger()
+        output_log_handler = logging.FileHandler(log_output_name)
+        output_log_formatter = logging.Formatter("%(asctime)s %(levelname)s %(message)s")
+        output_log_handler.setFormatter(output_log_formatter)
+        if not output_log.handlers:
+            output_log.addHandler(output_log_handler)
+        output_log.setLevel(logging_level_dict[logging_level])
+
+    if logging_stdout_output:
+        print_log = logging.getLogger()
+        print_log_handler = logging.StreamHandler()
+        print_log_formatter = logging.Formatter("%(asctime)s.%(msecs)03d %(levelname)s %(message)s", "%H:%M:%S")
+        print_log_handler.setFormatter(print_log_formatter)
+        if not print_log.handlers:
+            print_log.addHandler(print_log_handler)
+        print_log.setLevel(logging_level_dict[logging_level])
diff --git a/bitglitter/utilities/palette.py b/bitglitter/utilities/palette.py
new file mode 100644
index 0000000..8f86886
--- /dev/null
+++ b/bitglitter/utilities/palette.py
@@ -0,0 +1,166 @@
+from bitstring import BitArray, ConstBitStream
+import cv2
+import numpy
+
+import hashlib
+import itertools
+import logging
+import math
+from pathlib import Path
+from random import choice, randint
+
+from bitglitter.write.render.renderutilities import render_coords_generator
+
+
+class BitsToColor:
+    """This generates a dictionary linking a string binary value to an RGB value.  This is how binary data gets directly
+    converted to colors.  This step required more than a dictionary, as additional logic was required to switch between
+    a standard dictionary used by default and custom palettes, and 24 bit palettes.  They both convert data to colors in
+    different ways, and this provides a single clean interface for that.
+    """
+
+    def __init__(self, color_set_tupled, bit_length, palette_name):
+
+        logging.debug(f'Generating encoder for {palette_name}...')
+        self.color_set_tupled = color_set_tupled
+        self.bit_length = bit_length
+        self.return_value = self.generate_dictionary()
+
+    @staticmethod
+    def twenty_four_bit_values(bit_value):
+
+        red_channel = bit_value.read('uint : 8')
+        green_channel = bit_value.read('uint : 8')
+        blue_channel = bit_value.read('uint : 8')
+        return red_channel, green_channel, blue_channel
+
+    def generate_dictionary(self):
+
+        color_dict = {}
+        if self.bit_length != 24:
+
+            for value in range(len(self.color_set_tupled)):
+                temp_bin_holder = str(BitArray(uint=value, length=self.bit_length))
+                temp_bin_holder = ConstBitStream(temp_bin_holder)
+                color_dict[temp_bin_holder] = self.color_set_tupled[value]
+            return color_dict
+
+        else:
+            return self.twenty_four_bit_values
+
+    def get_color(self, value):
+
+        if self.bit_length != 24:
+            return self.return_value[value]
+        else:
+            return self.return_value(value)
+
+
+class ColorsToBits:
+    """This class does the exact opposite as BitsToColor.  This first generates a dictionary linking colors to
+    specific bit values, and then get_value() accomplishes that.  It is worth noting that 24 bit color functions
+    differently than the other color palettes, in that it doesn't use a dictionary, but rather converts each byte into
+    an unsigned integer for each of it's three color channels, and then returns that color.
+    """
+
+    def __init__(self, color_set_tupled, bit_length, palette_name):
+        logging.debug(f'Generating decoder for {palette_name}...')
+        self.color_set_tupled = color_set_tupled
+        self.bit_length = bit_length
+        self.return_value = self.generate_dictionary()
+
+    @staticmethod
+    def twenty_four_bit_values(color):
+        outgoing_data = BitArray()
+        for color_channel in color:
+            outgoing_data.append(BitArray(uint=color_channel, length=8))
+        return outgoing_data
+
+    def generate_dictionary(self):
+        value_dict = {}
+
+        if self.bit_length != 24:
+            for value in range(len(self.color_set_tupled)):
+                temp_bin_holder = str(BitArray(uint=value, length=self.bit_length))
+                temp_bin_holder = ConstBitStream(temp_bin_holder)
+                value_dict[self.color_set_tupled[value]] = temp_bin_holder
+            return value_dict
+        else:
+            return self.twenty_four_bit_values
+
+    def get_value(self, color):
+        if self.bit_length != 24:
+            return self.return_value[color]
+        else:
+            return self.return_value(color)
+
+
+def convert_hex_to_rgb(color_set):
+    """Takes the palette color set, converts any hex values into tuples, and returns a integer tuple of the R/G/B
+    color channels.
+    """
+
+    returned_list = []
+    for color in color_set:
+        if isinstance(color, str):  # is hex code
+            stripped = color.replace('#', '')
+            returned_list.append(tuple(int(stripped[i:i + 2], 16) for i in (0, 2, 4)))
+        else:  # Does nothing if already a tuple, simply adds it back to list to be returned.
+            returned_list.append(color)
+    return returned_list
+
+
+def get_color_distance(color_set):
+    """This function takes in the set of tuples in a palette, and calculates their proximity to each other in RGB space.
+    Higher number denote 'safer' palettes to use in the field, as they are less prone to errors in the field.  Getting 0
+    returned means you have at least a single pair of identical RGB values.  All colors must be unique!
+    """
+
+    min_distance = None
+    for pair in itertools.combinations(color_set, 2):
+        first_color, second_color = pair
+
+        red_distance = (second_color[0] - first_color[0]) ** 2
+        green_distance = (second_color[1] - first_color[1]) ** 2
+        blue_distance = (second_color[2] - first_color[2]) ** 2
+
+        sum_of_distances = math.sqrt(red_distance + green_distance + blue_distance)
+
+        if min_distance is not None:
+            if sum_of_distances < min_distance:
+                min_distance = sum_of_distances
+        else:
+            min_distance = sum_of_distances
+
+    return round(min_distance, 3)
+
+
+def get_palette_id_from_hash(name, description, time_created, color_set):
+    """Taking in the various parameters, this creates a unique ID for the custom palettes."""
+
+    # Normalizes list brackets to tuples to ensure consistent hashing across context/usage
+    color_set_string = str(color_set)
+    color_set_string.replace('[', '(')
+    color_set_string.replace(']', ')')
+
+    hasher = hashlib.sha256(str(name + description + str(time_created) + color_set_string).encode())
+    return hasher.hexdigest()
+
+
+def render_sample_frame(palette_name, palette_colors_rgb_tuple, is_24_bit, save_path, block_width, block_height,
+                        pixel_width):
+
+    image = numpy.zeros((pixel_width * block_height, pixel_width * block_width, 3), dtype='uint8')
+    next_coordinates = render_coords_generator(block_height, block_width, pixel_width, False)
+    for i in range(block_width * block_height):
+        if not is_24_bit:
+            chosen_color = choice(palette_colors_rgb_tuple)
+        else:
+            chosen_color = (randint(0, 255), randint(0, 255), randint(0, 255))
+        block_coordinates = next(next_coordinates)
+        cv2.rectangle(image, (block_coordinates[0], block_coordinates[1]), (block_coordinates[2], block_coordinates[3]),
+                      (chosen_color[2], chosen_color[1], chosen_color[0]), -1)
+
+    save_directory = Path(save_path)
+    save_path = save_directory / f'{palette_name}.png'
+    cv2.imwrite(str(save_path), image)
diff --git a/bitglitter/utilities/read.py b/bitglitter/utilities/read.py
new file mode 100644
index 0000000..4b4f6f7
--- /dev/null
+++ b/bitglitter/utilities/read.py
@@ -0,0 +1,7 @@
+from bitglitter.config.config import session
+from bitglitter.config.readmodels.readmodels import StreamFrame
+
+
+def flush_inactive_frames():
+    session.query(StreamFrame).filter(StreamFrame.is_complete == False).delete()
+    session.commit()
diff --git a/bitglitter/validation/__init__.py b/bitglitter/validation/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/bitglitter/validation/utilities.py b/bitglitter/validation/utilities.py
new file mode 100644
index 0000000..bca78e2
--- /dev/null
+++ b/bitglitter/validation/utilities.py
@@ -0,0 +1,70 @@
+import os
+import string
+
+
+def is_valid_directory(argument, path):
+    """Checks whether the inputted directory exists or not."""
+    if not os.path.isdir(path):
+        raise ValueError(f'Folder path "{path}" for {argument} does not exist.')
+
+
+def is_int_over_zero(argument, some_variable):
+    """Checks if variable is of type int and is greater than zero."""
+
+    if not isinstance(some_variable, int) or some_variable < 0:
+        raise ValueError(f"Argument {argument} must be an integer greater than zero, {some_variable} was provided")
+
+
+def proper_string_syntax(argument, inputted_string="", posix=False):
+    """This exists to verify various inputs are using allowed characters.  One such character that isn't allowed is
+    '\\', as that character is what the ASCII part of the stream header uses to divide attributes.
+    """
+
+    if posix:
+        acceptable_chars = string.ascii_letters + string.digits + string.punctuation.replace("\\", "") + " "
+    else:
+        acceptable_chars = string.ascii_letters + string.digits + '._-'
+
+    if posix:
+        if inputted_string[0] == ' ' or inputted_string[-1] == ' ':
+            raise ValueError('Cannot start or end string with space')
+
+    if inputted_string:
+        for letter in inputted_string:
+            if letter not in acceptable_chars:
+                if posix:
+                    raise ValueError(f'Can only use these characters for {argument}: {acceptable_chars}')
+                else:
+                    raise ValueError(f"Only ASCII characters excluding '\\' are allowed in {argument}.")
+
+
+def is_bool(argument, variable):
+    """Will raise error if argument isn't type bool.  Used in verifying arguments for read() and write()"""
+    if not isinstance(variable, bool):
+        raise ValueError(f'Argument {argument} must be type boolean.')
+
+
+def verify_write_params_output_mode(output_mode):
+    if output_mode != 'video' and output_mode != 'image':
+        raise ValueError("Argument output_mode in write() only accepts 'video' or 'image'.")
+
+
+def verify_write_params_scrypt(scrypt_n, scrypt_r, scrypt_p):
+    """Used both during """
+
+    is_int_over_zero('scrypt_n', scrypt_n)
+    is_int_over_zero('scrypt_r', scrypt_r)
+    is_int_over_zero('scrypt_p', scrypt_p)
+
+
+def logging_config_validate(logging_level, logging_stdout_output, logging_txt_output):
+    acceptable_level_words = [False, 'debug', 'info']
+    if logging_level not in acceptable_level_words:
+        raise ValueError(f"{logging_level} is not a valid input for logging_level.  Only 'info', 'debug', and False are"
+                         f" allowed.")
+
+    if not isinstance(logging_stdout_output, bool):
+        raise ValueError("Only booleans are allowed for logging_stdout_output.")
+
+    if not isinstance(logging_txt_output, bool):
+        raise ValueError("Only booleans are allowed for logging_txt_output.")
diff --git a/bitglitter/validation/validatepalette.py b/bitglitter/validation/validatepalette.py
new file mode 100644
index 0000000..442c768
--- /dev/null
+++ b/bitglitter/validation/validatepalette.py
@@ -0,0 +1,79 @@
+from bitglitter.utilities.palette import get_color_distance
+from bitglitter.validation.utilities import proper_string_syntax
+
+
+def palette_geometry_verify(stream_palette_bit_length, block_width, block_height, output_type, fps=0):
+    """This function calculates the necessary overhead for both images and videos for subsequent frames after 1.  It
+    returns a number between 0-100%, for what percentage the overhead occupies.  The lower the value, the higher the
+    frame efficiency.
+    """
+
+    total_blocks = block_width * block_height
+
+    INITIALIZER_OVERHEAD = block_height + block_width + 579
+    FRAME_HEADER_BITS = 352
+    occupied_blocks = 0
+
+    if output_type == 'image':
+        total_blocks -= INITIALIZER_OVERHEAD
+        occupied_blocks += INITIALIZER_OVERHEAD
+    bits_available_per_frame = (total_blocks * stream_palette_bit_length) - FRAME_HEADER_BITS
+    occupied_blocks += int(FRAME_HEADER_BITS / stream_palette_bit_length)
+
+    payload_frame_percentage = round((((total_blocks - occupied_blocks) / total_blocks) * 100), 2)
+
+    output_per_sec = 0
+    if output_type == 'video':
+        output_per_sec = bits_available_per_frame * fps
+
+    return payload_frame_percentage, bits_available_per_frame, output_per_sec
+
+
+def custom_palette_values_validate(name_string, description_string, color_set, read_mode=False):
+
+    proper_string_syntax(name_string)
+    proper_string_syntax(description_string)
+
+    if len(name_string) > 50:
+        if read_mode:
+            return False
+        raise ValueError('Custom palette names cannot exceed 50 characters.')
+    if len(description_string) > 100:
+        if read_mode:
+            return False
+        raise ValueError('Custom palette descriptions cannot exceed 100 characters.')
+    if len(color_set) > 256:
+        if read_mode:
+            return False
+        raise ValueError('Custom palettes cannot exceed 256 colors.')
+
+    # Verifying color set parameters.  2^n length, 3 values per color, values are type int, values are 0-255.
+    if len(color_set) % 2 != 0 or len(color_set) < 2:
+        if read_mode:
+            return False
+        raise ValueError(
+            "Length of color set must be 2^n length (2 colors, 4, 8, etc) with a minimum of two colors.")
+
+    for color_tuple in color_set:
+
+        if len(color_tuple) != 3:
+            if read_mode:
+                return False
+            raise ValueError("Each color needs 3 entries, for red green and blue.")
+
+        for color in color_tuple:
+            if not isinstance(color, int) or color < 0 or color > 255:
+                if read_mode:
+                    return False
+                raise ValueError("For each RGB value, it must be an integer between 0 and 255.")
+
+    # Finally, verify colors aren't overlapping (ie, the same color is used twice).
+    min_distance = get_color_distance(color_set)
+    if min_distance == 0:
+        if read_mode:
+            return False
+        raise ValueError("Calculated color distance is 0.  This occurs when you have two identical colors in your"
+                         "palette.  This breaks the communication protocol.  See BitGlitter guide for more "
+                         "information.")
+
+    return True
diff --git a/bitglitter/validation/validateread.py b/bitglitter/validation/validateread.py
new file mode 100644
index 0000000..69cb8bb
--- /dev/null
+++ b/bitglitter/validation/validateread.py
@@ -0,0 +1,90 @@
+import logging
+import os
+from pathlib import Path
+
+from bitglitter.config.config import session
+from bitglitter.config.configmodels import Constants
+from bitglitter.validation.utilities import is_valid_directory, is_int_over_zero, proper_string_syntax, \
+    is_bool
+
+
+def _file_path_validate(file_path, type_requirement, video_formats, image_formats):
+    path = Path(file_path)
+    if not path.is_absolute():
+        raise ValueError('Write argument input_path must be an absolute path to the file or directory.')
+    if not os.path.isfile(file_path):
+        raise ValueError(f'file_to_input argument {file_path} must be a file.')
+
+    file_format = os.path.splitext(file_path)[1]
+
+    if file_format in video_formats:
+        if type_requirement == 'image':
+            raise ValueError('Lists can only accept image files for input_file, videos must be decoded one at a time,'
+                             ' using type string.')
+        logging.debug(f'Video detected: {file_format}')
+        input_type = 'video'
+    elif file_format in image_formats:
+        logging.debug(f'Image detected: {file_format}')
+        input_type = 'image'
+    else:
+        if type_requirement == 'all':
+            raise ValueError(f'input_file value {file_path} is not a valid format.  Only the following are allowed: '
+                             f'{video_formats}, and {image_formats}')
+        elif type_requirement == 'image':
+            raise ValueError(f'input_file value {file_path} is not a valid format.  Only the following are allowed for'
+                             f'lists: {image_formats}')
+
+    return input_type
+
+
+def validate_read_parameters(file_path, output_path, encryption_key, scrypt_n, scrypt_r, scrypt_p,
+                             block_height_override, block_width_override, max_cpu_cores, save_statistics,
+                             bad_frame_strikes, stop_at_metadata_load, auto_unpackage_stream,
+                             auto_delete_finished_stream):
+    """This function verifies the arguments going into read() to ensure they comform with the required format for
+    processing.
+    """
+
+    logging.debug("Validating read parameters...")
+    constants = session.query(Constants).first()
+
+    valid_video_formats = constants.return_valid_video_formats()
+    valid_image_formats = constants.return_valid_image_formats()
+
+    if isinstance(file_path, str):  # Single video or image file to decode
+        path = Path(file_path)
+        if not path.is_dir():
+            input_type = _file_path_validate(file_path, 'all', valid_video_formats, valid_image_formats)
+        else:
+            input_type = 'image'
+    elif isinstance(file_path, list):  # Multiple images
+        for path in file_path:
+            input_type = _file_path_validate(path, 'image', valid_video_formats, valid_image_formats)
+    else:
+        raise ValueError('file_path can only accept strings for single video file or a directory (with images inside), '
+                         'or list of string for image frames.')
+
+    if output_path:
+        is_valid_directory('file_to_input', output_path)
+
+    proper_string_syntax('encryption_key', encryption_key)
+
+    is_int_over_zero('bad_frame_strikes', bad_frame_strikes)
+
+    is_int_over_zero('scrypt_n', scrypt_n)
+    is_int_over_zero('scrypt_r', scrypt_r)
+    is_int_over_zero('scrypt_p', scrypt_p)
+
+    is_int_over_zero('block_height_override', block_height_override)
+    is_int_over_zero('block_width_override', block_width_override)
+
+    if not isinstance(max_cpu_cores, int) or max_cpu_cores < 0:
+        raise ValueError('max_cpu_cores must be an integer greater than or equal to 0.')
+
+    is_bool('save_statistics', save_statistics)
+    is_bool('stop_at_metadata_load', stop_at_metadata_load)
+    is_bool('auto_unpackage_stream', auto_unpackage_stream)
+    is_bool('auto_delete_finished_stream', auto_delete_finished_stream)
+    logging.debug("Read parameters validated.")
+
+    return input_type
diff --git a/bitglitter/validation/validatewrite.py b/bitglitter/validation/validatewrite.py
new file mode 100644
index 0000000..a22e32c
--- /dev/null
+++ b/bitglitter/validation/validatewrite.py
@@ -0,0 +1,104 @@
+import logging
+from pathlib import Path
+
+from bitglitter.config.palettefunctions import _return_palette
+from bitglitter.utilities.display import humanize_file_size
+from bitglitter.validation.utilities import is_bool, is_int_over_zero, is_valid_directory, proper_string_syntax, \
+    verify_write_params_output_mode, verify_write_params_scrypt
+from bitglitter.validation.validatepalette import palette_geometry_verify
+
+
+def verify_write_params_render_values(stream_palette_id, stream_palette_nickname, pixel_width, block_height,
+                                      block_width, frames_per_second, output_mode, preset_validation):
+    palette = _return_palette(palette_id=stream_palette_id, palette_nickname=stream_palette_nickname)
+
+    is_int_over_zero('pixel_width', pixel_width)
+    is_int_over_zero('block_height', block_height)
+    is_int_over_zero('block_width', block_width)
+    is_int_over_zero('frames_per_second', frames_per_second)
+
+    if block_width < 5 or block_height < 5:
+        raise ValueError('Frames cannot be less than 5 blocks tall or wide.')
+
+    if block_width * block_height < 1500:
+        raise ValueError(f'Frames must have more than 1500 blocks, this current config has'
+                         f' {block_width * block_height}')
+
+    # TEMPORARY until issue is fixed
+    if output_mode == 'video' and palette.is_24_bit:
+        raise ValueError("24 bit palettes can not be used with videos at this time due to codec issues.  This is"
+                         "\nbeing worked on and will be restored soon!  This palette will still work on images.")
+
+    # With the given dimensions and bit length, is it sufficient?
+    payload_frame_percentage, bits_available_per_frame, output_per_sec = palette_geometry_verify(
+                                                                        palette.bit_length, block_width,
+                                                                        block_height, output_mode, frames_per_second)
+    if not preset_validation:
+        logging.info(f'{humanize_file_size(bits_available_per_frame)}/frame, or ~{payload_frame_percentage}% of'
+                     f' frames are allocated for the payload itself (higher is better)')
+
+        if output_mode == 'video':
+            logging.info(f'As a video, it will effectively be transporting '
+                         f'{humanize_file_size(round(output_per_sec / 8))}/sec of data.')
+        logging.info('Minimum geometry requirements met.')
+
+
+def write_parameter_validate(input_path, stream_name, stream_description, stream_output_path, stream_name_file_output,
+                             file_mask_enabled, encryption_key, max_cpu_cores=None, output_mode=None,
+                             compression_enabled=None, scrypt_n=None, scrypt_r=None, scrypt_p=None,
+                             stream_palette_id=None, stream_palette_nickname=None, pixel_width=None, block_height=None,
+                             block_width=None, frames_per_second=None, preset_used=False):
+    """This function verifies all write() parameters.  Look at this as the gatekeeper that stops invalid arguments from
+     proceeding through the process, potentially breaking the stream (or causing BitGlitter to crash).
+    """
+    logging.info("Verifying write parameters...")
+
+    path = Path(input_path)
+    if not path.is_absolute():
+        raise ValueError('Write argument input_path must be an absolute path to the file or directory.')
+    if not path.exists():
+        raise FileNotFoundError('File or directory for input_path does not exist.')
+
+    if not preset_used:
+        verify_write_params_render_values(stream_palette_id, stream_palette_nickname, pixel_width, block_height,
+                                          block_width, frames_per_second, output_mode, preset_validation=False)
+        verify_write_params_output_mode(output_mode)
+        is_bool('compression_enabled', compression_enabled)
+        verify_write_params_scrypt(scrypt_n, scrypt_r, scrypt_p)
+
+    proper_string_syntax(stream_name, 'stream_name', posix=True)
+    proper_string_syntax(stream_description, 'stream_description')
+    if len(stream_name) > 150:
+        raise ValueError('Stream names cannot exceed 150 characters')
+    is_bool('output_name', stream_name_file_output)
+    is_bool('file_mask_enabled', file_mask_enabled)
+    if not encryption_key and file_mask_enabled:
+        raise ValueError('file_mask_enabled can only be set to True if an encryption key is provided.')
+
+    if not stream_name:
+        raise ValueError('Write argument stream_name is required.')
+
+    if stream_output_path:
+        is_valid_directory('stream_output_path', stream_output_path)
+    verify_write_params_output_mode(output_mode)
+    logging.info("Write parameters validated.")
+
+    if not isinstance(max_cpu_cores, int) or max_cpu_cores < 0:
+        raise ValueError('max_cpu_cores must be an integer greater than or equal to 0.')
+
+
+def write_preset_validate(nickname, output_mode, compression_enabled, scrypt_n, scrypt_r, scrypt_p, max_cpu_cores,
+                          stream_palette_id, pixel_width, block_height, block_width, frames_per_second):
+    """Validates presets in a nice clean way."""
+
+    if not isinstance(nickname, str):
+        raise ValueError('Nickname for preset must be type str.')
+
+    if not isinstance(max_cpu_cores, int):
+        raise ValueError('cpu_cores for preset must be type int.')
+
+    is_bool('compression_enabled', compression_enabled)
+    verify_write_params_output_mode(output_mode)
+    verify_write_params_render_values(stream_palette_id, nickname, pixel_width, block_height, block_width,
+                                      frames_per_second, output_mode, preset_validation=True)
+    verify_write_params_scrypt(scrypt_n, scrypt_r, scrypt_p)
diff --git a/bitglitter/write/__init__.py b/bitglitter/write/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/bitglitter/write/preprocess/__init__.py b/bitglitter/write/preprocess/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/bitglitter/write/preprocess/fileprocess.py b/bitglitter/write/preprocess/fileprocess.py
new file mode 100644
index 0000000..7edab87
--- /dev/null
+++ b/bitglitter/write/preprocess/fileprocess.py
@@ -0,0 +1,101 @@
+import logging
+import os
+
+from bitglitter.utilities.compression import compress_file
+from bitglitter.utilities.cryptography import encrypt_file, get_hash_from_file
+
+
+def directory_crawler(directory_path, payload_directory, compression_enabled, crypto_key, scrypt_n,
+                      scrypt_r, scrypt_p):
+    logging.info(f'Scanning {directory_path}...')
+    manifest = {}
+    # Directory keys:
+    # n = directory name
+    # f = files in that directory (not including subdirectories)
+    # s = subdirectories (only accessible from the given directory, not subdirectories of subdirectories)
+
+    manifest['n'] = directory_path.name
+
+    files = []
+    subdirectories = []
+    sorted_glob = sorted(directory_path.glob('*'))
+    for item in sorted_glob:
+        if item.is_file():
+            files.append(item)
+        else:
+            subdirectories.append(item)
+
+    file_manifests = []
+    if files:
+        for file in files:
+            returned_manifest = process_file(file, payload_directory, crypto_key, scrypt_n, scrypt_r, scrypt_p,
+                                             compression_enabled)
+            file_manifests.append(returned_manifest)
+        manifest['f'] = file_manifests
+
+    directory_manifests = []
+    if subdirectories:
+        for subdirectory in subdirectories:
+            returned_manifest = directory_crawler(subdirectory, payload_directory, compression_enabled, crypto_key,
+                                                  scrypt_n, scrypt_r, scrypt_p)
+            directory_manifests.append(returned_manifest)
+        manifest['s'] = directory_manifests
+
+    return manifest
+
+
+def process_file(file_abs_path, payload_directory, crypto_key, scrypt_n, scrypt_r, scrypt_p, compression_enabled):
+    manifest = {}
+    # File keys:
+    # fn = file name
+    # rs = raw file size
+    # rh = raw file hash
+    # ps = processed file size (only if compression or crypto)
+    # ph = processed file hash (only if compression or crypto)
+
+    file_name = file_abs_path.name
+    manifest['fn'] = file_name
+    logging.info(f'Found: {file_name}')
+    raw_file_size = file_abs_path.stat().st_size
+    manifest['rs'] = raw_file_size
+    raw_file_hash = get_hash_from_file(file_abs_path)
+    manifest['rh'] = raw_file_hash
+
+    if compression_enabled or crypto_key:
+
+        if compression_enabled:
+            compressed_file_path = payload_directory / 'temp_compressed.bin'
+            compress_file(file_abs_path, compressed_file_path, 'write', remove_input=False)
+            active_processing_path = compressed_file_path
+        if crypto_key:
+            encrypted_file_path = payload_directory / 'temp_encrypted.bin'
+            if compression_enabled:
+                encrypt_file(compressed_file_path, encrypted_file_path, 'write', crypto_key, scrypt_n, scrypt_r,
+                             scrypt_p, remove_input=True)
+            else:
+                encrypt_file(file_abs_path, encrypted_file_path, 'write', crypto_key, scrypt_n, scrypt_r, scrypt_p,
+                             remove_input=False)
+            active_processing_path = encrypted_file_path
+
+        processed_file_size = active_processing_path.stat().st_size
+        processed_file_hash = get_hash_from_file(active_processing_path)
+        manifest['ps'] = processed_file_size
+        manifest['ph'] = processed_file_hash
+    else:
+        active_processing_path = file_abs_path
+
+    stream_payload_write_path = payload_directory / 'processed.bin'
+
+    with open(stream_payload_write_path, 'ab') as byte_write:
+        with open(active_processing_path, 'rb') as byte_read:
+            chunk_size = 1000000
+            while True:
+                chunk = byte_read.read(chunk_size)
+                if chunk:
+                    byte_write.write(chunk)
+                else:
+                    break
+    if compression_enabled or crypto_key:
+        os.remove(active_processing_path)
+
+    return manifest
diff --git a/bitglitter/write/preprocess/preprocessor.py b/bitglitter/write/preprocess/preprocessor.py
new file mode 100644
index 0000000..9af7eea
--- /dev/null
+++ b/bitglitter/write/preprocess/preprocessor.py
@@ -0,0 +1,39 @@
+import logging
+from pathlib import Path
+import time
+
+from bitglitter.config.configmodels import CurrentJobState
+from bitglitter.utilities.display import humanize_file_size
+from bitglitter.utilities.cryptography import get_hash_from_file
+from bitglitter.utilities.filemanipulation import refresh_directory, return_file_size
+from bitglitter.write.preprocess.fileprocess import directory_crawler, process_file
+
+
+class PreProcessor:
+    """PreProcessor performs all necessary preparations for the file(s) to be written, prior to rendering.  After all
+    files and directories to be rendered are processed and ready to go in a binary file, a few other attributes are
+    processed such as stream size and hash, that will be added into the headers.
+    """
+
+    def __init__(self, working_directory, input_path, crypto_key, compression_enabled, scrypt_n, scrypt_r, scrypt_p):
+        self.datetime_started = int(time.time())
+        self.active_folder = refresh_directory(working_directory)
+        self.encryption_enabled = True if crypto_key else False
+        logging.info("Preprocess initializing...")
+
+        input_path = Path(input_path)
+        if input_path.is_file():
+            self.manifest = process_file(input_path, working_directory, crypto_key, scrypt_n, scrypt_r,
+                                         scrypt_p, compression_enabled)
+        else:
+            self.manifest = directory_crawler(input_path, working_directory, compression_enabled, crypto_key,
+                                              scrypt_n, scrypt_r, scrypt_p)
+
+        self.processed_binary_path = working_directory / 'processed.bin'
+        self.stream_sha256 = get_hash_from_file(self.processed_binary_path)
+        logging.info(f"Stream SHA-256 Hash: {self.stream_sha256}")
+        CurrentJobState.new_task(self.stream_sha256)
+        self.size_in_bytes = return_file_size(self.processed_binary_path)
+        logging.info(f'Pre-Processed Payload Size: {humanize_file_size(self.size_in_bytes)}')
+        logging.debug(f'^ ({self.size_in_bytes} B)')
+        logging.info("Preprocess complete.")
diff --git a/bitglitter/write/render/__init__.py b/bitglitter/write/render/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/bitglitter/write/render/framestategenerator.py b/bitglitter/write/render/framestategenerator.py
new file mode 100644
index 0000000..6cbfee9
--- /dev/null
+++ b/bitglitter/write/render/framestategenerator.py
@@ -0,0 +1,140 @@
+from bitstring import BitStream, ConstBitStream
+
+import logging
+from pathlib import Path
+
+from bitglitter.write.render.headerencode import initializer_header_encode, frame_header_encode
+
+
+def frame_state_generator(block_height, block_width, pixel_width, protocol_version, initializer_palette,
+                          stream_palette, output_mode, stream_output_path, stream_name_file_output, working_directory,
+                          total_frames, stream_header, metadata_header, palette_header, stream_sha256,
+                          initializer_palette_dict, initializer_palette_dict_b, stream_palette_dict,
+                          default_output_path, stream_name, save_statistics, total_operations):
+    """This function iterates over the pre-processed data, and assembles and renders the frames.  There are plenty of
+    comments in this function that describe what each part is doing, to follow along.
+    """
+
+    # Determining output for images.
+    if output_mode == 'image':
+        if stream_output_path:
+            image_output_path = Path(stream_output_path)
+        else:
+            image_output_path = default_output_path
+
+    if output_mode == 'video':
+        image_output_path = working_directory
+
+    # Constants
+    TOTAL_BLOCKS = block_height * block_width
+    INITIALIZER_CALIBRATOR_BLOCK_OVERHEAD = (block_height + block_width - 1) + 580  # calibrator + initializer
+    INITIALIZER_BIT_OVERHEAD = 580
+    FRAME_HEADER_BIT_OVERHEAD = 352
+
+    #  Final preparations for stream header parts.
+    stream_payload = ConstBitStream(filename=working_directory / 'processed.bin')
+
+    #  These are the headers before stream palette is used, to be perfectly clear
+    pre_stream_palette_headers_merged = BitStream()
+    pre_stream_palette_headers_merged.append(BitStream(stream_header))
+    pre_stream_palette_headers_merged.append(BitStream(metadata_header))
+    pre_stream_palette_headers_merged.append(palette_header)
+    pre_stream_palette_headers_merged = ConstBitStream(pre_stream_palette_headers_merged)
+
+    frame_number = 1
+
+    # This is the primary loop; it will yield until it traverses the entire file.
+    while stream_payload.bitpos != stream_payload.length:
+
+        logging.debug(f'GENERATOR:  Generating frame data for {frame_number} of {total_frames} ...')
+
+        initializer_bits = BitStream()
+        setup_headers_bits = BitStream()
+        payload_bits = BitStream()
+        padding_bits = BitStream()
+        stream_sha_bytes = bytes.fromhex(stream_sha256)
+
+        max_allowable_payload_bits = len(stream_payload) - stream_payload.bitpos
+        max_allowable_pre_stream_palette_header_bits = pre_stream_palette_headers_merged.length \
+                                                       - pre_stream_palette_headers_merged.bitpos
+        blocks_left_this_frame = TOTAL_BLOCKS
+        initializer_enabled = False
+        initializer_palette_blocks_used = 0
+        last_frame = False
+        setup_headers_terminate_this_frame = False
+
+        if frame_number == 1 or output_mode == 'image':
+            initializer_bits = initializer_header_encode(block_height, block_width, protocol_version, stream_palette,
+                                                         stream_sha_bytes)
+            initializer_palette_blocks_used += INITIALIZER_BIT_OVERHEAD
+            blocks_left_this_frame -= INITIALIZER_CALIBRATOR_BLOCK_OVERHEAD
+            initializer_enabled = True
+
+        # Pre stream palette headers to be rendered on these frames
+        if pre_stream_palette_headers_merged.bitpos != pre_stream_palette_headers_merged.length:
+            blocks_left_this_frame -= FRAME_HEADER_BIT_OVERHEAD
+            initializer_palette_blocks_used += FRAME_HEADER_BIT_OVERHEAD
+
+            # Pre stream palette headers don't have enough room to finish on this frame.
+            if blocks_left_this_frame <= max_allowable_pre_stream_palette_header_bits:
+                setup_headers_bits = pre_stream_palette_headers_merged.read(blocks_left_this_frame)
+                initializer_palette_blocks_used += blocks_left_this_frame
+
+            # Pre stream palette headers have enough room to finish on this frame.
+            else:
+                setup_headers_terminate_this_frame = True
+                setup_headers_bits = pre_stream_palette_headers_merged.read(
+                    max_allowable_pre_stream_palette_header_bits)
+                initializer_palette_blocks_used += max_allowable_pre_stream_palette_header_bits
+                blocks_left_this_frame -= max_allowable_pre_stream_palette_header_bits
+
+                # There is room on this pre stream palette header termination frame to start writing the payload
+                if blocks_left_this_frame:
+                    bits_available_this_frame = blocks_left_this_frame * stream_palette.bit_length
+                    payload_bits = stream_payload.read(min(bits_available_this_frame, max_allowable_payload_bits))
+                    # The payload will finish on this frame.
+                    if bits_available_this_frame >= max_allowable_payload_bits:
+                        last_frame = True
+
+        # Standard frame containing only the payload, frame header, and initializer if enabled.
+        else:
+            bits_available_this_frame = (blocks_left_this_frame * stream_palette.bit_length) - FRAME_HEADER_BIT_OVERHEAD
+            payload_bits = stream_payload.read(min(bits_available_this_frame, max_allowable_payload_bits))
+
+            # Payload will finish on this frame.
+            if bits_available_this_frame >= max_allowable_payload_bits:
+                last_frame = True
+
+        frame_hashable_bits = BitStream(setup_headers_bits + payload_bits)
+        combined_frame_length = frame_hashable_bits.len + FRAME_HEADER_BIT_OVERHEAD
+
+        # On the last frame, there may be excess bit capacity in the final block.  This pads the payload so it cleanly
+        # fits into the final block.
+        if last_frame:
+            # Not counting the full frame, but rather the part with just the stream palette:
+            if setup_headers_terminate_this_frame:
+                remainder_bits = stream_palette.bit_length - (payload_bits.len % stream_palette.bit_length) \
+                    if payload_bits.len % stream_palette.bit_length else 0
+            else:
+                remainder_bits = stream_palette.bit_length - (combined_frame_length % stream_palette.bit_length) \
+                    if combined_frame_length % stream_palette.bit_length else 0
+            if remainder_bits == stream_palette.bit_length:
+                remainder_bits = 0
+
+            padding_bits = BitStream(bin=f"{'0' * remainder_bits}")
+            frame_hashable_bits.append(padding_bits)
+
+        frame_header_holder = frame_header_encode(frame_hashable_bits.tobytes(), frame_hashable_bits.len, frame_number)
+        merged_pieces = initializer_bits + frame_header_holder + setup_headers_bits + payload_bits + padding_bits
+        frame_payload = ConstBitStream(merged_pieces)
+
+        yield {'block_height': block_height, 'block_width': block_width, 'pixel_width': pixel_width, 'frame_payload':
+               frame_payload, 'initializer_palette_blocks_used': initializer_palette_blocks_used, 'stream_palette_dict':
+               stream_palette_dict, 'stream_palette_bit_length': stream_palette.bit_length, 'initializer_palette_dict':
+               initializer_palette_dict, 'initializer_palette_dict_b': initializer_palette_dict_b,
+               'initializer_palette': initializer_palette, 'output_mode': output_mode, 'stream_name_file_output':
+               stream_name_file_output, 'initializer_enabled': initializer_enabled, 'frame_number': frame_number,
+               'total_frames': total_frames, 'image_output_path': image_output_path, 'stream_sha256': stream_sha256,
+               'stream_name': stream_name, 'save_statistics': save_statistics, 'total_operations': total_operations}
+
+        frame_number += 1
diff --git a/bitglitter/write/render/headerencode.py b/bitglitter/write/render/headerencode.py
new file mode 100644
index 0000000..7f3d06f
--- /dev/null
+++ b/bitglitter/write/render/headerencode.py
@@ -0,0 +1,174 @@
+from bitstring import BitArray, BitStream, ConstBitStream
+import cv2
+
+import json
+import logging
+import zlib
+
+from bitglitter.utilities.compression import compress_bytes
+from bitglitter.utilities.cryptography import encrypt_bytes, get_sha256_hash_from_bytes
+
+
+def calibrator_header_render(image, block_height, block_width, pixel_width, initializer_palette_dict,
+                             initializer_palette_dict_b):
+    """This creates the checkboard-like pattern along the top and left of the first frame of video streams, and every
+    frame of image streams.  This is what the reader uses to initially lock onto the frame.  Stream block_width and
+    block_height are encoded into this pattern, using alternating color palettes so no two repeating values produce a
+    continuous block of color, interfering with the frame lock process."""
+
+    cv2.rectangle(image, (0, 0), (pixel_width - 1, pixel_width - 1), (0, 0, 0), -1)
+
+    block_width_encoded = BitArray(uint=block_width, length=block_width - 1)
+    block_width_encoded.reverse()
+    readable_block_width = ConstBitStream(block_width_encoded)
+
+    for i in range(block_width - 1):
+        next_bit = readable_block_width.read('bits : 1')
+
+        if i % 2 == 0:
+            color_value = initializer_palette_dict_b.get_color(ConstBitStream(next_bit))
+
+        else:
+            color_value = initializer_palette_dict.get_color(ConstBitStream(next_bit))
+
+        cv2.rectangle(image,
+                      (pixel_width * i + pixel_width, 0),
+                      (pixel_width * (i + 1) - 1 + pixel_width, pixel_width - 1),
+                      (color_value[2], color_value[1], color_value[0]), -1)
+
+    block_height_encoded = BitArray(uint=block_height, length=block_height - 1)
+    block_height_encoded.reverse()
+    readable_block_height = ConstBitStream(block_height_encoded)
+
+    for i in range(block_height - 1):
+        next_bit = readable_block_height.read('bits : 1')
+
+        if i % 2 == 0:
+            color_value = initializer_palette_dict_b.get_color(ConstBitStream(next_bit))
+
+        else:
+            color_value = initializer_palette_dict.get_color(ConstBitStream(next_bit))
+
+        cv2.rectangle(image,
+                      (0, pixel_width * i + pixel_width),
+                      (pixel_width - 1, pixel_width * (i + 1) - 1 + pixel_width),
+                      (color_value[2], color_value[1], color_value[0]), -1)
+
+    return image
+
+
+def initializer_header_encode(block_height, block_width, protocol_version, stream_palette, stream_sha256):
+    """This generates the initializer header, which is present in black and white colors on the top of the first frame
+    of video streams, and every frame of image streams.  It provides import information on stream geometry as well as
+    protocol version and what palettes are being used.
+    """
+
+    logging.debug('Generating initialization header...')
+
+    adding_bits = BitArray()
+    adding_bits.append(BitArray(uint=protocol_version, length=4))
+    adding_bits.append(BitArray(uint=block_height, length=16))
+    adding_bits.append(BitArray(uint=block_width, length=16))
+
+    if not stream_palette.is_custom:
+        adding_bits.append(BitArray(uint=int(stream_palette.palette_id), length=256))
+    else:
+        adding_bits.append(BitArray(hex=stream_palette.palette_id))
+
+    adding_bits.append(BitArray(bytes=stream_sha256, length=256))
+
+    full_bit_string_to_hash = adding_bits.tobytes()
+    crc_output = zlib.crc32(full_bit_string_to_hash)
+    adding_bits.append(BitArray(uint=crc_output, length=32))
+    assert len(adding_bits.bin) == 580
+    logging.debug('Initialization header generated.')
+
+    return adding_bits
+
+
+def frame_header_encode(frame_payload_bytes, unpadded_bit_length, frame_number):
+    """This is ran on the top of every frame (after the initializer if used) to give important information specifically
+    for this frame, such as how many bits to scan, frame number, and its SHA-256 hash.
+    """
+
+    adding_bits = BitArray()
+    adding_bits.append(BitArray(uint=unpadded_bit_length, length=32))
+    adding_bits.append(BitArray(uint=frame_number, length=32))
+    byte_hash = get_sha256_hash_from_bytes(frame_payload_bytes, byte_output=True)
+    adding_bits.append(BitArray(bytes=byte_hash, length=256))
+
+    full_bit_string_to_hash = adding_bits.tobytes()
+    crc_output = zlib.crc32(full_bit_string_to_hash)
+    adding_bits.append(BitArray(uint=crc_output, length=32))
+    assert len(adding_bits.bin) == 352
+    logging.debug('Frame header generated.')
+
+    return adding_bits
+
+
+def stream_header_encode(size_in_bytes, total_frames, compression_enabled, encryption_enabled,
+                         file_mask_enabled, metadata_header_length, metadata_header_hash,
+                         custom_palette_header_length, custom_palette_header_hash):
+    """The stream header precedes the payload, and contains 2nd priority orientation data."""
+
+    adding_bits = BitStream()
+
+    adding_bits.append(BitArray(uint=size_in_bytes, length=64))
+    adding_bits.append(BitArray(uint=total_frames, length=32))
+    adding_bits.append(BitArray([int(compression_enabled), int(encryption_enabled),
+                                 int(file_mask_enabled if encryption_enabled else False)]))
+
+    adding_bits.append(BitArray(uint=metadata_header_length, length=32))
+    adding_bits.append(BitArray(bytes=metadata_header_hash, length=256))
+
+    adding_bits.append(BitArray(uint=custom_palette_header_length, length=10))
+    if custom_palette_header_hash:
+        adding_bits.append(BitArray(bytes=custom_palette_header_hash, length=256))
+    else:
+        adding_bits.append(BitArray(bytes=bytearray(32), length=256))
+    to_bytes = adding_bits.tobytes()
+    crc_output = zlib.crc32(to_bytes)
+    adding_bits.append(BitArray(uint=crc_output, length=32))
+
+    assert len(adding_bits.bin) == 685
+
+    logging.debug('Stream header generated.')
+    return ConstBitStream(adding_bits)
+
+
+def metadata_header_encode(file_mask_enabled, crypto_key, scrypt_n, scrypt_r, scrypt_p, bg_version, stream_name,
+                           time_created, stream_description, manifest):
+    """This header contains the file manifest, and other data not crucial for stream orientation."""
+
+    logging.debug('Building metadata header...')
+    json_manifest = json.dumps(manifest)
+    logging.debug(f'JSON manifest: {json_manifest}')
+    meta_data_string = "\\\\".join([bg_version, stream_name, stream_description, str(time_created), json_manifest])
+    logging.debug(f'Text Header merged: {meta_data_string}')
+
+    raw_header_to_bytes = bytes(meta_data_string, 'UTF-8')
+    raw_header_hash_bytes = get_sha256_hash_from_bytes(raw_header_to_bytes, byte_output=True)
+    processed_header_bytes = compress_bytes(raw_header_to_bytes)
+
+    if file_mask_enabled and crypto_key:
+        logging.debug('Encrypting metadata header...')
+        processed_header_bytes = encrypt_bytes(processed_header_bytes, crypto_key, scrypt_n, scrypt_r, scrypt_p)
+
+    logging.debug('Metadata header generated.')
+    logging.debug(f'Processed metadata length: {len(processed_header_bytes)}')
+    return processed_header_bytes, raw_header_hash_bytes
+
+
+def custom_palette_header_encode(palette):
+    """This header is ran after the stream setup header whenever a custom palette is used for the stream."""
+
+    # adding_bits = BitStream()
+    text_string_to_bytes = bytes('\\\\'.join([palette.palette_id, palette.name, palette.description,
+                                              str(palette.time_created), str(palette.number_of_colors),
+                                              str(palette.convert_colors_to_tuple())]), 'utf-8')
+
+    raw_header_hash_bytes = get_sha256_hash_from_bytes(text_string_to_bytes, byte_output=True)
+    compressed_header_bytes = compress_bytes(text_string_to_bytes)
+
+    logging.debug('Palette initialization header generated.')
+    return compressed_header_bytes, raw_header_hash_bytes
diff --git a/bitglitter/write/render/renderhandler.py b/bitglitter/write/render/renderhandler.py
new file mode 100644
index 0000000..d79b1da
--- /dev/null
+++ b/bitglitter/write/render/renderhandler.py
@@ -0,0 +1,90 @@
+import logging
+from multiprocessing import cpu_count, Pool
+
+from bitglitter.config.palettefunctions import _return_palette
+from bitglitter.utilities.filemanipulation import create_default_output_folder
+from bitglitter.write.render.headerencode import metadata_header_encode, custom_palette_header_encode, \
+    stream_header_encode
+from bitglitter.write.render.framestategenerator import frame_state_generator
+from bitglitter.write.render.renderutilities import draw_frame, total_frames_estimator
+
+
+class RenderHandler:
+
+    def __init__(self,
+
+                 # Basic Setup
+                 stream_name, stream_description, working_dir, default_output_path, crypto_key, scrypt_n, scrypt_r,
+                 scrypt_p,
+
+                 # Stream Rendering
+                 block_height, block_width, pixel_width, stream_palette_id, max_cpu_cores,
+
+                 # Header
+                 stream_sha256, size_in_bytes, compression_enabled, encryption_enabled, file_mask_enabled,
+                 datetime_started, bg_version, manifest, protocol_version,
+
+                 # Render Output
+                 output_mode, output_path, stream_name_file_output,
+
+                 # Statistics
+                 save_statistics
+
+                 ):
+
+        self.blocks_wrote = 0
+        self.total_frames = 0
+
+        # Pre render
+        logging.info('Beginning pre-render processes...')
+        create_default_output_folder(default_output_path)
+        initializer_palette = _return_palette(palette_id='1')
+        initializer_palette_b = _return_palette('11')
+        stream_palette = _return_palette(palette_id=stream_palette_id)
+
+        initializer_palette_dict = initializer_palette.return_encoder()
+        initializer_palette_dict_b = initializer_palette_b.return_encoder()
+        stream_palette_dict = stream_palette.return_encoder()
+
+        metadata_header_bytes, metadata_header_hash_bytes = metadata_header_encode(file_mask_enabled, crypto_key,
+                                                                                   scrypt_n, scrypt_r, scrypt_p,
+                                                                                   bg_version, stream_name,
+                                                                                   datetime_started,
+                                                                                   stream_description, manifest)
+        palette_header_bytes = b''
+        palette_header_hash_bytes = b''
+        if stream_palette.is_custom:
+            palette_header_bytes, palette_header_hash_bytes = custom_palette_header_encode(stream_palette)
+
+        self.total_frames = total_frames_estimator(block_height, block_width, len(metadata_header_bytes),
+                                                   len(palette_header_bytes), size_in_bytes, stream_palette,
+                                                   output_mode)
+
+        stream_header = stream_header_encode(size_in_bytes, self.total_frames, compression_enabled,
+                                             encryption_enabled, file_mask_enabled, len(metadata_header_bytes),
+                                             metadata_header_hash_bytes, len(palette_header_bytes),
+                                             palette_header_hash_bytes)
+        logging.info('Pre-render complete.')
+
+        #  Render
+        if max_cpu_cores == 0 or max_cpu_cores >= cpu_count():
+            pool_size = cpu_count()
+        else:
+            pool_size = max_cpu_cores
+
+        self.total_operations = self.total_frames * (1 + int(output_mode != 'image'))
+
+        with Pool(processes=pool_size) as worker_pool:
+            logging.info(f'Beginning rendering on {pool_size} CPU core(s)...')
+
+            for frame_encode in worker_pool.imap(draw_frame,
+                                                 frame_state_generator(block_height, block_width, pixel_width,
+                                                 protocol_version, initializer_palette, stream_palette, output_mode,
+                                                 output_path, stream_name_file_output, working_dir, self.total_frames,
+                                                 stream_header, metadata_header_bytes, palette_header_bytes,
+                                                 stream_sha256, initializer_palette_dict, initializer_palette_dict_b,
+                                                 stream_palette_dict, default_output_path, stream_name,
+                                                 save_statistics, self.total_operations), chunksize=1):
+                pass
+
+        logging.info('Rendering frames complete.')
diff --git a/bitglitter/write/render/renderutilities.py b/bitglitter/write/render/renderutilities.py
new file mode 100644
index 0000000..d6b6f44
--- /dev/null
+++ b/bitglitter/write/render/renderutilities.py
@@ -0,0 +1,150 @@
+from bitstring import ConstBitStream
+import cv2
+import numpy
+
+import logging
+import math
+from pathlib import Path
+
+from bitglitter.write.render.headerencode import calibrator_header_render
+
+
+def total_frames_estimator(block_height, block_width, metadata_header_length, palette_header_length, size_in_bytes,
+                           stream_palette, output_mode):
+    """This method returns how many frames will be required to complete the rendering process."""
+
+    logging.debug("Calculating how many frames to render...")
+
+    # Constants
+    TOTAL_BLOCKS_PER_FRAME = block_height * block_width
+    CALIBRATOR_BLOCK_OVERHEAD = block_height + block_width - 1
+    INITIALIZER_BIT_OVERHEAD = 580
+    FRAME_HEADER_BIT_OVERHEAD = 352
+    STREAM_HEADER_BIT_OVERHEAD = 685
+    pre_stream_data_left_bits = STREAM_HEADER_BIT_OVERHEAD + ((palette_header_length + metadata_header_length) * 8)
+    logging.info(f'palette_header_length {palette_header_length} metadata_header_length {metadata_header_length}')
+    payload_data_left_bits = size_in_bytes * 8
+    STREAM_PALETTE_BIT_LENGTH = stream_palette.bit_length
+
+    total_frames = 1
+
+    while pre_stream_data_left_bits:
+        remaining_blocks_this_frame = TOTAL_BLOCKS_PER_FRAME - FRAME_HEADER_BIT_OVERHEAD - ((INITIALIZER_BIT_OVERHEAD +
+                                         CALIBRATOR_BLOCK_OVERHEAD) * int(total_frames == 1 or output_mode == 'image'))
+        if remaining_blocks_this_frame < pre_stream_data_left_bits:
+            pre_stream_data_left_bits -= remaining_blocks_this_frame
+            total_frames += 1
+        else:
+            remaining_blocks_this_frame -= pre_stream_data_left_bits
+            pre_stream_data_left_bits = 0
+            payload_data_left_bits = max(0, payload_data_left_bits - (remaining_blocks_this_frame *
+                                                                      STREAM_PALETTE_BIT_LENGTH))
+
+    if payload_data_left_bits:
+        remaining_blocks = TOTAL_BLOCKS_PER_FRAME - ((INITIALIZER_BIT_OVERHEAD + CALIBRATOR_BLOCK_OVERHEAD)
+                                                     * int(output_mode == 'image'))
+        payload_bits_per_frame = (remaining_blocks * STREAM_PALETTE_BIT_LENGTH) - FRAME_HEADER_BIT_OVERHEAD
+        total_frames += math.ceil(payload_data_left_bits / payload_bits_per_frame)
+
+    logging.info(f'{total_frames} frame(s) required for this write process.')
+    return total_frames
+
+
+def render_coords_generator(block_height, block_width, pixel_width, initializer_enabled):
+    """This generator yields the coordinates for each of the blocks used, depending on the geometry of the frame."""
+
+    for y_range in range(block_height - int(initializer_enabled)):
+        for x_range in range(block_width - int(initializer_enabled)):
+            yield ((pixel_width * int(initializer_enabled)) + (pixel_width * x_range),
+                   (pixel_width * int(initializer_enabled)) + (pixel_width * y_range),
+                   (pixel_width * int(initializer_enabled)) + (pixel_width * (x_range + 1) - 1),
+                   (pixel_width * int(initializer_enabled)) + (pixel_width * (y_range + 1) - 1))
+
+
+def draw_frame(dict_obj):
+    """Unpacking dictionary object into variables for easier reading of function.  A single argument must be passed
+    here because multiprocessing's imap requires it.
+    """
+
+    block_height = dict_obj['block_height']
+    block_width = dict_obj['block_width']
+    pixel_width = dict_obj['pixel_width']
+    frame_payload = dict_obj['frame_payload']
+    initializer_palette_blocks_used = dict_obj['initializer_palette_blocks_used']
+    stream_palette_dict = dict_obj['stream_palette_dict']
+    stream_palette_bit_length = dict_obj['stream_palette_bit_length']
+    initializer_palette_dict = dict_obj['initializer_palette_dict']
+    initializer_palette_dict_b = dict_obj['initializer_palette_dict_b']
+    initializer_palette = dict_obj['initializer_palette']
+    output_mode = dict_obj['output_mode']
+    stream_name_file_output = dict_obj['stream_name_file_output']
+    stream_name = dict_obj['stream_name']
+    initializer_enabled = dict_obj['initializer_enabled']
+    frame_number = dict_obj['frame_number']
+    total_frames = dict_obj['total_frames']
+    image_output_path = dict_obj['image_output_path']
+    stream_sha256 = dict_obj['stream_sha256']
+    save_statistics = dict_obj['save_statistics']
+    total_operations = dict_obj['total_operations']
+
+    percentage_string = f'{round(((frame_number / total_operations) * 100), 2):.2f}'
+    logging.info(f'Generating {frame_number} of {total_frames}... {percentage_string} %')
+
+    image = numpy.zeros((pixel_width * block_height, pixel_width * block_width, 3), dtype='uint8')
+
+    if initializer_enabled:
+        image = calibrator_header_render(image, block_height, block_width, pixel_width, initializer_palette_dict,
+                                         initializer_palette_dict_b)
+
+    next_coordinates = render_coords_generator(block_height, block_width, pixel_width, initializer_enabled)
+    block_position = 0
+    while len(frame_payload) != frame_payload.bitpos:
+
+        # Primary palette selection (ie, header_palette or stream_palette depending on where we are in the stream)
+        if block_position >= initializer_palette_blocks_used:
+            active_palette_dict, bit_read_length = stream_palette_dict, stream_palette_bit_length
+
+        # Initializer palette selection
+        elif block_position < initializer_palette_blocks_used:
+            active_palette_dict, bit_read_length = (initializer_palette_dict, initializer_palette.bit_length)
+
+        # Here to signal something has broken.
+        else:
+            raise RuntimeError('Something has gone wrong in matching block position to palette.  This state'
+                               '\nis reached only if something is broken.')
+
+        # This is loading the next bit(s) to be written in the frame, and then converting it to an RGB value.
+        next_bits = frame_payload.read(f'bits : {bit_read_length}')
+        color_value = active_palette_dict.get_color(ConstBitStream(next_bits))
+
+        # With the color loaded, we'll get the coordinates of the next block (top left + bottom corner), and draw it in.
+        block_coordinates = next(next_coordinates)
+        cv2.rectangle(image, (block_coordinates[0], block_coordinates[1]), (block_coordinates[2], block_coordinates[3]),
+                      (color_value[2], color_value[1], color_value[0]), -1)
+        block_position += 1
+
+    # Frames get saved as .png files.
+    frame_number_to_string = str(frame_number)
+
+    if output_mode == 'video':
+        file_name = frame_number_to_string
+
+    else:
+        if stream_name_file_output:
+            file_name = stream_name + ' - ' + str(frame_number)
+        else:
+            file_name = stream_sha256 + ' - ' + str(frame_number)
+
+    # save_path = Path(image_output_path / f'{str(file_name)}.png')
+    cv2.imwrite(str(Path(image_output_path / f'{str(file_name)}.png')), image)
+
+    # Ensure every bit in payload is accounted for.
+    assert frame_payload.len == frame_payload.bitpos
+
+    if save_statistics:
+        from bitglitter.config.configfunctions import write_stats_update
+        if frame_number != total_frames:
+            blocks_wrote = block_height * block_width
+        else:
+            blocks_wrote = block_position
+        write_stats_update(blocks_wrote, 1, frame_payload.len)
diff --git a/bitglitter/write/render/videorender.py b/bitglitter/write/render/videorender.py
new file mode 100644
index 0000000..56ab59f
--- /dev/null
+++ b/bitglitter/write/render/videorender.py
@@ -0,0 +1,33 @@
+import cv2
+
+import logging
+from pathlib import Path
+
+
+def render_video(output_path, stream_name_file_output, working_directory, total_frames,
+                 frames_per_second, stream_sha256, block_width, block_height, pixel_width, stream_name,
+                 total_operations):
+    """Taking in whichever arguments, it takes all of the rendered frames, and merges them into an .mp4 ."""
+
+    logging.info('Rendering video...')
+
+    if stream_name_file_output:
+        video_name = stream_name
+    else:
+        video_name = stream_sha256
+
+    frame_size = (block_width * pixel_width, block_height * pixel_width)
+    save_path = f"{Path(output_path / video_name)}.mp4"
+
+    # This needs to be tuned- consistently very large files
+    output = cv2.VideoWriter(str(save_path), cv2.VideoWriter.fourcc(*'mp4v'), frames_per_second, frame_size)
+
+    for frame in range(total_frames):
+        percentage_string = f'{round((((frame + 1) / total_operations) * 100) + 50, 2):.2f}'
+        logging.info(f'Rendering video frame {frame + 1} of {total_frames}... {percentage_string} %')
+        image = cv2.imread(str(Path(working_directory / f'{frame + 1}.png')))
+        output.write(image)
+
+    output.release()
+    logging.info('Rendering video complete.')
+    logging.info(f'Video save path: {save_path}')
diff --git a/bitglitter/write/renderhandler.py b/bitglitter/write/renderhandler.py
deleted file mode 100644
index 975a464..0000000
--- a/bitglitter/write/renderhandler.py
+++ /dev/null
@@ -1,95 +0,0 @@
-import logging
-import shutil
-
-from bitglitter.config.config import config
-from bitglitter.palettes.paletteutilities import palette_grabber, ValuesToColor
-
-
-class RenderHandler:
-    '''This is where the rendering process is set up, and fed into the appropriate protocol objects and objects.  While
-    this may seem like an unnecessary module currently (as of v1.0), I think it's purpose will become more apparent as
-    we need a uniform place to handle various future protocols.
-    '''
-
-    def __init__(self,
-
-                 # Initializer
-                 protocol, block_height, block_width, header_palette_id,
-
-                 # Frame Header
-                 stream_sha,
-
-                 # Stream Header - Binary Preamble
-                 size_in_bytes, compression_enabled, encryption_enabled, file_mask_enabled, date_created,
-                 stream_palette_id,
-
-                 # Stream Header - ASCII Encoded
-                 bg_version, stream_name, stream_description, post_encryption_hash,
-
-                 # Render argument
-                 pixel_width, output_mode, stream_output_path, frames_per_second,
-
-                 # Misc
-                 active_folder, pass_through
-                 ):
-
-        logging.debug('RenderHandler initializing...')
-
-        self.protocol = protocol
-        self.block_height = block_height
-        self.block_width = block_width
-
-        self.stream_sha = stream_sha
-
-        self.size_in_bytes = size_in_bytes
-        self.compression_enabled = compression_enabled
-        self.encryption_enabled = encryption_enabled
-        self.file_mask_enabled = file_mask_enabled
-        self.date_created = date_created
-
-        self.bg_version = bg_version
-        self.stream_name = stream_name
-        self.stream_description = stream_description
-        self.post_encryption_hash = post_encryption_hash
-
-        self.pixel_width = pixel_width
-        self.output_mode = output_mode
-        self.stream_output_path = stream_output_path
-        self.frames_per_second = frames_per_second
-
-        self.active_path = active_folder
-        self.pass_through = pass_through
-
-        self.header_palette = palette_grabber(header_palette_id)
-        self.stream_palette = palette_grabber(stream_palette_id)
-        self.initializer_palette = palette_grabber('1')
-
-        self.header_palette_dict = ValuesToColor(self.header_palette, 'header_palette')
-        self.stream_palette_dict = ValuesToColor(self.stream_palette, 'stream_palette')
-        self.initializer_palette_dict = ValuesToColor(self.initializer_palette, 'initializer_palette')
-
-        self.protocol.begin_session_process(self.active_path, self.pass_through, self.stream_output_path,
-                                            self.bg_version, self.initializer_palette, self.header_palette,
-                                            self.stream_palette, self.initializer_palette_dict,
-                                            self.header_palette_dict, self.stream_palette_dict, self.block_height,
-                                            self.block_width, self.pixel_width, self.frames_per_second,
-                                            self.output_mode, self.stream_sha, self.size_in_bytes,
-                                            self.compression_enabled, self.encryption_enabled, self.file_mask_enabled,
-                                            self.date_created, self.stream_name, self.stream_description,
-                                            self.post_encryption_hash)
-
-        config.stats_handler.write_update(((self.protocol.total_frames - 1) * (self.block_height * self.block_width) +
-                                         self.protocol.remainder_blocks), self.protocol.total_frames,
-                                          self.size_in_bytes)
-
-        self.cleanup()
-
-
-    def cleanup(self):
-        '''Removes temporary folder.'''
-
-        logging.debug("Deleting temporary folder....")
-        shutil.rmtree(self.active_path)
-
-        config.save_session()
-        logging.info('Write process complete!')
\ No newline at end of file
diff --git a/bitglitter/write/rendervideo.py b/bitglitter/write/rendervideo.py
deleted file mode 100644
index fb6c8a6..0000000
--- a/bitglitter/write/rendervideo.py
+++ /dev/null
@@ -1,16 +0,0 @@
-import logging
-import time
-
-import ffmpeg
-
-def render_video(stream_output_path, date_created, image_output_path, frame_number_formatted, frames_per_second):
-    '''Taking in whichever arguments, it takes all of the rendered frames, and merges them into an .mp4 .'''
-    logging.info('Rendering video...')
-    video_output_path = ''
-    if stream_output_path:
-        video_output_path = stream_output_path + '\\'
-    logging.info(f'{video_output_path}{time.strftime("%Y-%m-%d %H-%M-%S", time.localtime(date_created))}.mp4')
-    (ffmpeg
-     .input(f'{image_output_path}%{len(frame_number_formatted)}d.png', framerate=frames_per_second)
-     .output(f'{video_output_path}{time.strftime("%Y-%m-%d %H-%M-%S", time.localtime(date_created))}.mp4')
-     .run())
\ No newline at end of file
diff --git a/bitglitter/write/write.py b/bitglitter/write/write.py
index 6b124e9..59f2c8b 100644
--- a/bitglitter/write/write.py
+++ b/bitglitter/write/write.py
@@ -1,76 +1,120 @@
-from bitglitter.config.constants import BG_VERSION, WRITE_PATH, SCRYPT_N_DEFAULT, SCRYPT_R_DEFAULT, SCRYPT_P_DEFAULT, \
-    HEADER_PALETTE_ID, STREAM_PALETTE_ID, PIXEL_WIDTH, BLOCK_HEIGHT, BLOCK_WIDTH, FRAMES_PER_SECOND
-from bitglitter.config.loggingset import logging_setter
-from bitglitter.write.renderhandler import RenderHandler
-
-
-def write(   # Basic setup
-             file_list,
-             stream_name ="",
-             stream_description ="",
-             output_path = False,
-             output_mode ="video",
-
-             # Stream configuration
-             compression_enabled = True,
-             file_mask_enabled = False,
-
-             # Encryption
-             encryption_key ="",
-             scrypt_override_n = SCRYPT_N_DEFAULT,
-             scrypt_override_r = SCRYPT_R_DEFAULT,
-             scrypt_override_p = SCRYPT_P_DEFAULT,
-
-             # Stream geometry, color
-             #protocol_version = '1' # Currently disabled, will enable once multiple choices of protocols are available.
-             header_palette_id = HEADER_PALETTE_ID,
-             stream_palette_id = STREAM_PALETTE_ID,
-             pixel_width = PIXEL_WIDTH,
-             block_height = BLOCK_HEIGHT,
-             block_width = BLOCK_WIDTH,
-
-             # Video rendering
-             frames_per_second = FRAMES_PER_SECOND,
-
-             # Logging
-             logging_level ='info',
-             logging_screen_output = True,
-             logging_save_output = False,
-             ):
-    '''This is the primary function in creating BitGlitter streams from files.  Please see Wiki page for more
-    information.
-    '''
-
-    # Logging initializing.
-    logging_setter(logging_level, logging_screen_output, logging_save_output)
-
-    # Loading write protocol.  This import function is here deliberately because of logging.
-    from bitglitter.protocols.protocolhandler import protocol_handler
-    write_protocol = protocol_handler.return_write_protocol('1')
-
-    # Are all parameters acceptable?
-    write_protocol.verify_write_parameters(file_list, stream_name, stream_description, output_path, output_mode,
-                                           compression_enabled, file_mask_enabled, scrypt_override_n, scrypt_override_r,
-                                           scrypt_override_p, stream_palette_id, header_palette_id, pixel_width,
-                                           block_height, block_width, frames_per_second)
-
-    # This sets the name of the temporary folder while the file is being written.
-    active_path = WRITE_PATH
+from pathlib import Path
+
+from bitglitter.config.config import session
+from bitglitter.config.configmodels import Config, Constants
+from bitglitter.config.presetfunctions import return_preset
+from bitglitter.utilities.filemanipulation import refresh_directory, remove_working_folder
+from bitglitter.utilities.loggingset import logging_setter
+from bitglitter.validation.validatewrite import write_parameter_validate
+from bitglitter.write.preprocess.preprocessor import PreProcessor
+from bitglitter.write.render.renderhandler import RenderHandler
+from bitglitter.write.render.videorender import render_video
+
+
+def write(
+
+        # Basic setup
+        input_path,
+        preset_nickname=None,
+        stream_name="",
+        stream_description="",
+        output_directory=None,
+        output_mode="video",
+        stream_name_file_output=False,
+        max_cpu_cores=0,
+
+        # Stream configuration
+        compression_enabled=True,
+        # error_correction=False, -> Pending further research for viability
+
+        # Encryption
+        encryption_key="",
+        file_mask_enabled=False,
+        scrypt_n=14,
+        scrypt_r=8,
+        scrypt_p=1,
+
+        # Stream geometry, color, general config
+        stream_palette_id='6',
+        stream_palette_nickname=None,
+        pixel_width=24,
+        block_height=45,
+        block_width=80,
+
+        # Video rendering
+        frames_per_second=30,
+
+        # Logging
+        logging_level='info',
+        logging_stdout_output=True,
+        logging_txt_output=False,
+
+        # Session Data
+        save_statistics=False,
+):
+    """Creates BitGlitter streams from files/directories.  See repo readme for more information."""
+
+    config = session.query(Config).first()
+    constants = session.query(Constants).first()
+
+    # This sets the name of the temporary folder while the file is being written, as well as the default output path.
+    working_directory = Path(constants.WORKING_DIR)
+    refresh_directory(working_directory)
+
+    # Setting save path for stream
+    if output_directory:
+        output_directory = Path(output_directory)
+    else:
+        output_directory = Path(config.write_path)
+        refresh_directory(output_directory, delete=False)
+
+    print(output_directory)
+
+    # Initializing logging, must be up front for logging to work properly.
+    logging_setter(logging_level, logging_stdout_output, logging_txt_output, Path(config.log_txt_dir))
+
+    # Loading preset (if given), and validating any other parameters before continuing with the rendering process.
+    if preset_nickname:
+        write_parameter_validate(input_path, stream_name, stream_description, output_directory, stream_name_file_output,
+                                 file_mask_enabled, encryption_key, preset_used=True)
+        preset = return_preset(preset_nickname)
+        output_mode = preset.output_mode
+        compression_enabled = preset.compression_enabled
+        scrypt_n = preset.scrypt_n
+        scrypt_r = preset.scrypt_r
+        scrypt_p = preset.scrypt_p
+        max_cpu_cores = preset.max_cpu_cores
+        stream_palette_id = preset.stream_palette_id
+        pixel_width = preset.pixel_width
+        block_height = preset.block_height
+        block_width = preset.block_width
+        frames_per_second = preset.frames_per_second
+    else:
+        write_parameter_validate(input_path, stream_name, stream_description, output_directory, stream_name_file_output,
+                                 file_mask_enabled, encryption_key, max_cpu_cores, output_mode, compression_enabled,
+                                 scrypt_n, scrypt_r, scrypt_p, stream_palette_id, stream_palette_nickname, pixel_width,
+                                 block_height, block_width, frames_per_second, preset_used=False)
 
     # This is what takes the raw input files and runs them through several processes in preparation for rendering.
-    pre_process = write_protocol.pre_processing(active_path, file_list, encryption_key, file_mask_enabled,
-                                                compression_enabled, scrypt_override_n, scrypt_override_r,
-                                                scrypt_override_p)
-
-    # After the data is prepared, this is what renders the data into images.
-    frame_processor = write_protocol.frame_processor()
-
-    renderHandler = RenderHandler(frame_processor, block_height, block_width, header_palette_id, pre_process.stream_sha,
-                                  pre_process.size_in_bytes, compression_enabled, encryption_key != "",
-                                  file_mask_enabled, pre_process.date_created, stream_palette_id, BG_VERSION,
-                                  stream_name, stream_description, pre_process.post_encryption_hash, pixel_width,
-                                  output_mode, output_path, frames_per_second, active_path, pre_process.pass_through)
-
-
-    # Returns the SHA of the preprocessed file in string format for optional storage of it.
-    return pre_process.stream_sha
+    pre_processor = PreProcessor(working_directory, input_path, encryption_key, compression_enabled, scrypt_n, scrypt_r,
+                                 scrypt_p)
+
+    # This is where the final steps leading up to frame generation as well as generation itself takes place.
+    render_handler = RenderHandler(stream_name, stream_description, working_directory, output_directory, encryption_key,
+                                   scrypt_n, scrypt_r, scrypt_p, block_height, block_width, pixel_width,
+                                   stream_palette_id, max_cpu_cores, pre_processor.stream_sha256,
+                                   pre_processor.size_in_bytes, compression_enabled, pre_processor.encryption_enabled,
+                                   file_mask_enabled, pre_processor.datetime_started, constants.BG_VERSION,
+                                   pre_processor.manifest, constants.PROTOCOL_VERSION, output_mode, output_directory,
+                                   stream_name_file_output, save_statistics)
+
+    # Video render
+    if output_mode == 'video':
+        render_video(output_directory, stream_name_file_output, working_directory, render_handler.total_frames,
+                     frames_per_second, pre_processor.stream_sha256, block_width, block_height, pixel_width,
+                     stream_name, render_handler.total_operations)
+
+    # Removing temporary files
+    remove_working_folder(working_directory)
+
+    return pre_processor.stream_sha256
diff --git a/setup.py b/setup.py
index 762e215..cc6801c 100644
--- a/setup.py
+++ b/setup.py
@@ -4,27 +4,25 @@
     long_description = openReadMe.read()
 
 setup(
-    name="BitGlitter",
-    version="1.0.3",
+    name="BitGlitter Python",
+    version="2.0.0",
     author="Mark Michon",
-    author_email="markmichon7@gmail.com",
+    author_email="markkmichon@gmail.com",
     description="⚡ Embed data payloads inside of ordinary images or video, through high performance 2-D matrix codes.",
     long_description=long_description,
     long_description_content_type="text/markdown",
-    url="https://github.com/MarkMichon1/BitGlitter",
+    url="https://github.com/MarkMichon1/BitGlitter-Python",
     packages=find_namespace_packages(),
     install_requires=[
-        'bitstring',
-        'cryptography',
-        'ffmpeg-python',
-        'opencv-python',
-        'Pillow',
-        'argparse',
+        "bitstring==3.1.9",
+        "cryptography==3.4.8",
+        "opencv-python==4.5.3.56",
+        "SQLAlchemy==1.4.25"
     ],
+    extra_require={"dev": "pytest"},
     classifiers=[
-        "Programming Language :: Python :: 3.7",
+        "Programming Language :: Python :: 3.9.5",
         "License :: OSI Approved :: MIT License",
         "Operating System :: OS Independent",
     ],
-    entry_points={'console scripts' : ['bitglitter = bitglitter.__main__:cli']}
-)
\ No newline at end of file
+)


Test patch:
diff --git a/bitglitter/tests/__init__.py b/bitglitter/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/bitglitter/tests/palettes/__init__.py b/bitglitter/tests/palettes/__init__.py
new file mode 100644
index 0000000..a8ccb01
--- /dev/null
+++ b/bitglitter/tests/palettes/__init__.py
@@ -0,0 +1,1 @@
+"""This must be revisited at a later point.  All code is pre v2.0"""
\ No newline at end of file
diff --git a/bitglitter/tests/test_coloranalysis.py b/bitglitter/tests/palettes/test_coloranalysis.py
similarity index 90%
rename from bitglitter/tests/test_coloranalysis.py
rename to bitglitter/tests/palettes/test_coloranalysis.py
index 4f025c0..7de315a 100644
--- a/bitglitter/tests/test_coloranalysis.py
+++ b/bitglitter/tests/palettes/test_coloranalysis.py
@@ -1,5 +1,5 @@
 import unittest
-from bitglitter.read.coloranalysis import color_snap, return_distance
+from bitglitter.read.framescan.scanutilities import color_snap
 
 
 class Test(unittest.TestCase):
diff --git a/bitglitter/tests/palettes/test_paletteutilites.py b/bitglitter/tests/palettes/test_paletteutilites.py
new file mode 100644
index 0000000..f44c895
--- /dev/null
+++ b/bitglitter/tests/palettes/test_paletteutilites.py
@@ -0,0 +1,77 @@
+from bitglitter.utilities.palette import get_color_distance
+
+
+def test_color_distance_with_default_1bit_palette():
+    t_palette = ((0,0,0), (255,255,255))
+    t_expect = 441.67
+    t_result = get_color_distance(t_palette)
+    assert len(t_palette) == 2 # number of colors
+    assert t_expect == t_result
+
+def test_color_distance_with_alternate_1bit_palette():
+    t_palette = ((255, 0, 255), (0, 255, 255))
+    t_expect = 360.62
+    t_result = get_color_distance(t_palette)
+    assert len(t_palette) == 2 # number of colors
+    assert t_expect == t_result
+
+def test_color_distance_with_default_2bit_palette():
+    t_palette = ((0,0,0), (255,0,0), (0,255,0), (0,0,255))
+    t_expect = 255
+    t_result = get_color_distance(t_palette)
+    assert len(t_palette) == 4 # number of colors
+    assert t_expect == t_result
+
+def test_color_distance_with_alternate_2bit_palette():
+    t_palette = ((0, 0, 0), (255, 255, 0), (0, 255, 255), (255, 0, 255))
+    t_expect = 360.62
+    t_result = get_color_distance(t_palette)
+    assert len(t_palette) == 4 # number of colors
+    assert t_expect == t_result
+
+
+def test_color_distance_with_default_3bit_palette():
+    t_palette = ((0,0,0), (255,0,0), (0,255,0), (0,0,255), (255,255,0), (0,255,255),
+                (255,0,255), (255,255,255))
+    t_expect = 255
+    t_result = get_color_distance(t_palette)
+    assert len(t_palette) == 8 # number of colors
+    assert t_expect == t_result
+
+def test_color_distance_with_default_4bit_palette():
+    t_palette = ((0,0,0), (128,128,128), (192,192,192), (128,0,0), (255,0,0), (128,128,0), 
+                (255,255,0), (0,255,0), (0,128,128), (0,128,0), (0,0,128), (0,0,255), 
+                (0,255,255), (128,0,128), (255,0,255), (255,255,255))
+    t_expect = 109.12
+    t_result = get_color_distance(t_palette)
+    assert len(t_palette) == 16 # number of colors
+    assert t_expect == t_result
+
+def test_color_distance_with_default_6bit_palette():
+    t_palette = ((0,0,0), (0,0,85), (0,0,170), (0,0,255), (0,85,0), (0,85,85), 
+                (0,85,170), (0,85,255), (0,170,0), (0,170,85),(0,170,170), 
+                (0,170,255), (0,255,0), (0,255,85), (0,255,170), (0,255,255), 
+                (85,0,0),(85,0,85), (85,0,170), (85,0,255), (85,85,0), (85,85,85), 
+                (85,85,170), (85,85,255),(85,170,0), (85,170,85), (85,170,170), 
+                (85,170,255), (85,255,0), (85,255,85), (85,255,170), (85,255,255), 
+                (170,0,0), (170,0,85), (170,0,170), (170,0,255), (170,85,0), 
+                (170,85,85),(170,85,170), (170,85,255), (170,170,0), (170,170,85), 
+                (170,170,170), (170,170,255),(170,255,0), (170,255,85), (170,255,170), 
+                (170,255,255), (255,0,0), (255,0,85), (255,0,170), (255,0,255), 
+                (255,85,0), (255,85,85), (255,85,170), (255,85,255), (255,170,0), 
+                (255,170,85), (255,170,170), (255,170,255), (255,255,0), (255,255,85), 
+                (255,255,170), (255,255,255))
+    t_expect = 85
+    t_result = get_color_distance(t_palette)
+    assert len(t_palette) == 64 # number of colors
+    assert t_expect == t_result
+
+
+def test_color_distance_with_duplicate_values():
+    t_palette = ((0,0,0), (255,0,0), (0,255,0), (0,0,0))
+    t_expect = 0
+    t_result = get_color_distance(t_palette)
+    assert len(t_palette) == 4 # number of colors
+    assert t_expect == t_result
+
+