5.2 KiB
ShrinkRay/SR/.sr*
The What
This is the first entry into the ODynaMIC container format family. ODynaMIC (Open Dynamic Media Interchange Container) is intended to be an easily extendable set of media container formats that offers multi level open optimizations and easy tooling to free media from format restrictions while enabling users to continue using the systems they are familiar with.
SR is intended for artists working with pixel art and other artfully restricted mediums. This is done primarily to reduce network traffic and make transferring media over very low bandwidth networks (in our case LoRa) more feasible.
Currently it provides the .sr and .srb (ShrinkRay Battery) formats, both are for pixel art but Battery provides a frames/layer system which in turn provides an animation system
The Why
Having worked with images in a serious capacity for about 15 years I've always wanted something smaller and easier to work with then existing formats. Many formats are easy to work with on the basis of an end user/power user but if you really want to alter things/make specific optimizations you'll have to write image processing code in many cases and in some cases you're simply out of luck. While languages like Processing and tools like Pillow make this relatively simple I wanted something even simpler, smaller and easier to extend/modify/etc.
The How
In order to achieve the aforementioned goals we leverage some novel optimizations: index collapsing, bitflip arrays and similar frame reduction.
Index collapsing
Take a 3x3 image as an example, everything but the center middle pixel is white.
With index collapsing we auto index as we go so the first pixel would give us an index of [(0,0,0)]
which can be reduced to [0]
since all three numbers are the same, the pixels array at this stage would look like this [0]
the 0 referring to the index of the color.
Stepping forward since the next pixel is also white we skip indexing, see that the last pixel was white and instead of adding another 0 we start building our first range. A range is an array of 2 values: the number of times to repeat the index reference and the index reference itself. Our pixel array now looks like this [[2, 0]]
.
We keep stepping through the next pixels, the next two are the same so our pixels array looks like this [[4, 0]]
.
We are now on the center middle pixel. Since this is a new color we index it. Our index now looks like [0, 255]
(our color can be collapsed to 255 because it is (255, 255, 255), this can be reduced further to 1 with a special flag if desired). Our pixel array now looks like [[4, 0], 1]
All we have left is more white pixels so after we step through those this is our final index [0, 255]
and this is our final pixel array [[4, 0], 1, [4, 0]]
. This representation is more efficient but this is further optimized with bitflip arrays.
Bitflip arrays
While we use msgpack for other optimizations this is a place where we can optimize further then their representation can because we have less generic needs. Bitflip arrays are a bytes representation of an array which handles part of the array being 2 values. It works like this:
We step through the array and convert each int value into bytes directly, bitshift left (leaving a 0 bit at the end) and append those bytes to the result bytes. When we ecounter an array we convert the int like before and bitshift left but flip the last bit to be 1, then append it to the bytes array and add the next value of the array as we do a normal int.
When we are reading the bytes for the array we reach byte by byte and check the last bit of each byte. If it's a 0 we bitshift right, convert the bytes to an int and append that to the pixel array. If it's a 1 we bitshift right, convert the bytes to an int, then get the next byte, convert that to an int, add both to an array and append that array to the pixel array.
After this we rebuild the pixel array by using the index and values and ranges from the temporary pixel array.
Very briefly here, I want to point out that this process is in fact lossless, in the literal sense that no data is lost in any way (if you don't believe me check the code), but as you'll see later, the optimizations here can make ShrinkRay outperform existing lossless formats.
Similar frame reduction
Similar frame reduction is the simplest technique involved but greatly helps with reducing the size of animations. We iterate over the frames, compare the current frame to the first frame using ImageChops.difference and store only the difference and it's bounding box. This way when compositing we can overlay the difference on the base image (using the bounding box to determine the offset) to create the original frame. This will be changed to be additive soon but this optimization works well at the moment.
Tests
In our tests on pixel art we see around 15% gains in compression at base with further gains depending on the composition/color usage of the art
Command line tool usage
Using SR is rather standard, this is both a command line tool and a library, first here's the --help section generated by the binary: