⚡ Embed data payloads inside of ordinary images or video, through high performance 2-D matrix codes.
Python Library (you are here) | Electron Desktop App | Python Backend For App
âš¡ Store and transfer files using high-performance animated barcodes
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.
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.
- 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 in size, or ~4.3 billion rendered frames. Put simply, there's no practical limit to your stream's size.
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|
In addition to downloading the code from Github, you can also grab it directly from PyPI:
pip install bitglitter
###The 2 Core Functions of BitGlitter
Ignoring the bells and whistles for now, all you need to use BitGlitter is
read(). Both use an assortment
of default arguments to remove a lot of the complexity starting out.
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.
write() -- converting files and directories into BitGlitter streams
We'll go a bit more in depth now.
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.
input_path is an absolute path pointing to the file or directory you'd like convert into a stream.
preset_nickname=None takes in a string name for a preset. Learn more below in Preset Functions.
stream_name='' required argument to name your stream, which will be encoded into the metadata of the stream when
read. 150 character limit.
stream_description='' serves as a text field to optionally put a description for the stream. No character limit.
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.
output_mode='video' controls the type of output you will have created. Your two choices are
'image'. Video outputs a single .mp4 file, whereas image output returns all of the frames.
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.
max_cpu_cores=0 determines how many CPU cores you'd like to use when rendering frames. 0 is default, which is
compression_enabled=True enables or disables compression of your data, prior to rendering into frames.
This is enabled by default.
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.
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.
scrypt_P=1 allow you to customize the parameters of the
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
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.
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.
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).
block_width=80 sets how many blocks wide the frame will be. By default this is set to 80.
frames_per_second=30 sets how many frames per second the video will play at, assuming
output_mode = "video" is used.
Finally we have several arguments to control logging.
logging_level='info' determines what level logging messages get outputted. It accepts three arguments-
default and only shows core status data during
'debug' shows info level messages as well as
debug messages from the various processes. Boolean
False disables logging altogether.
logging_stdout_output=True sets whether logging messages are displayed on the screen or not. Only accepts booleans.
Enabled by default.
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
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.
These default values transmit data at about 80 KB/s. This is a safe starting point that should be pretty resistant to corruption.
read() -- converting BitGlitter streams back into directories and files
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.
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.
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).
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
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.
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.
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.
max_cpu_cores=0 determines the amount of CPU cores to use, like
write(). The default value of 0
sets it to maximum available.
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.
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.
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
with attempt_password(). See Read Functions
below for more information.
arguments as well. These are seen in
write() as well; read their descriptions above to see what these do.
Custom Color Palette Functions
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 encoded share strings.
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).
palette_nameis the its name which will it will be saved as. It has a max length of 50 characters.
palette_descriptionis an optional field to include a brief description that will be attached with it. It has a max length of 100 characters.
color_setIs 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 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.
nicknameis 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.
return_palette(palette_id=None, palette_nickname=None) Returns a dictionary object of all of the palettes values.
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.
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.
import_palette_base64(base64_string) Import palettes from a unique share code (see directly above).
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_nickname) or all palettes saved.
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.
return_all_palettes() Returns a list of dictionary objects of all palettes in your database.
return_default_palettes() Returns a list of dictionary objects of all default palettes in your database.
return_custom_palettes() Returns a list of dictionary objects of all custom palettes in your database.
remove_custom_palette(palette_id, nickname) Deletes a custom palette.
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.
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.
remove_all_custom_palettes() Deletes all custom palettes, leaving only the default (hardwired) palettes.
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
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
Final note before proceeding- many of these functions you'll see
stream_sha256; this is a string of the stream's
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)
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
True will return all state data (for development, debugging, or if you're just curious). Returns
False if SHA-256
matches no existing stream.
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.
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
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.
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.
ffiles in that directory (not including subdirectories),
ssubdirectories for that given directory.
rsraw file size (its true size),
rhraw file hash (its true SHA-256 hash),
psprocessed file size (its packaged size when being transmitted),
phprocessed 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.
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.
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.
remove_all_partial_save_data() Removes all stream reads from the database.
update_stream_read(stream_sha256, auto_delete_finished_stream=None, auto_unpackage_stream=None) Is where you can
update behavior when the stream unpackages.
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.
return_all_blacklist_sha256() Returns a list of all blacklisted SHA-256 hashes as strings.
remove_blacklist_sha256(stream_sha256) Removes a specific SHA-256 hash. Returns
False depending on whether
remove_all_blacklist_sha256() Removes all SHA-256 hashes from the blacklist.
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.
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.
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.
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.
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.
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.
return_preset_data(nickname) returns a dictionary object returning the full state of the preset.
return_all_preset_data() returns a list of all of the presets as dictionaries.
remove_preset(nickname) removes the preset with the
nickname you gave it.
remove_all_presets() removes all saved presets.
output_stats() Returns a dictionary object displaying the following data for both reads and writes: blocks processed,
frames processed, data processed.
clear_stats() All statistics reset back to zero.
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.
return_settings() Returns a dictionary object of all settings.
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.
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
numpyare 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.
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.
Drop in and say hi on the Discord server:
There are a few points worth bringing up on using this library:
- 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.
Please use the issues page or let me know on Discord if there is something not working.
The third party libraries that make BitGlitter possible:
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
Â© 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 rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Hashes for BitGlitter-2.0.0-py3-none-any.whl