Discuss Scratch

awesome-llama
Scratcher
1000+ posts

Developing an image file format suitable for scratch projects

A lot of progress has been made.

Please see this repo: https://github.com/awesome-llama/TextImage



As I realised creating some of my projects, image editor and 3D terrain generator, FB3D, and TM3D, there's not really any good format for storing bitmap images and there is also no standardisation apart from importing and exporting lists of numbers or copy/pasting character-separated strings. At least there has been a shift recently to store colours as base64.

I'm posting to share what I have come up with and get feedback.

The format that I want to develop has quite a few constraints.
  • Firstly, it must be made entirely of printable characters, specifically those seen in the ASCII character set. It must also be a string. This leaves 94 characters (space is excluded). Both of these points ensure that scratch projects can read from an input using the ask block and save images so that a user can copy from a single list item. This also ensures text editors can view and edit these files.
  • Secondly, it must be sufficient for most purposes in Scratch. That means full colour support at least, an optional alpha channel, any dimensions, and be compressible. I will come back to compressibility soon.
  • Thirdly, it must be easy to implement in Scratch as otherwise no one would want to support it. Base64 png is cool but it's not really practical for example.
  • It must be lossless. Lossy images are out of the scope of what I am working on as it'll limit the use of the format to be mainly for photos and while that's still quite useful, lossless has many more uses that should be addressed (images can contain other data for example).

Here's an example, with some parts not finalised:
[magic number],x:20,y:10,scanlinedirection:1,bitdepth:8,[image data goes here]  
  • There will be a magic number but I don't know what it will be yet.
  • A major feature of this format is that it is structured through commas separating parts. The parts can be in any order as well and different parts are optional giving more freedom. The colons also separate the name of the “tag” and the data associated with it. I chose this structure as it is easy to implement importing in scratch - just iterate over every character and every time a comma or colon is encountered, save the previously seen characters to a list or two. Then the process of reading an image becomes just accessing the list items.
  • x and y are the dimensions
  • scanlinedirection is because some people handle images by starting from the top (like reading english text) and others start from the bottom (I do it to match scratch's coordinate system) Edit: I've changed my mind, I rather that be outside the scope of the format and the encoder or decoder handles it.
  • bitdepth is self explanatory
  • storing a colour palette would be another option but that needs to be decided too Edit: It's not worth it, especially if the colours can be accessed by a table

As for the image data, I have been thinking of storing the channels separately rather than combined however a combined option could exist. I'm not sure.
Compression is important as copy-pasting has its limitations and doing so on megabytes of text for larger images is not that user-friendly. I want to do something similar to deflate though it needs to be done on characters rather than bits.
Something worth pointing out is that 8-bit-per-channel RGB (24-bits total) can be compressed using a set of 64 digits down to only requiring 4 digits. Compare this with hexadecimal which requires 6 digits. A 67% reduction is good. See this project I made that demonstrates a basic example: https://scratch.mit.edu/projects/608063557/

Last edited by awesome-llama (Jan. 17, 2024 10:08:43)

crabraveprogrammer
Scratcher
100+ posts

Developing an image file format suitable for scratch projects

Don't use commas to separate things. You have so many extra characters, that you can have multiple separation characters, allowing you to sneak in extra information.
Arctenik
Scratcher
18 posts

Developing an image file format suitable for scratch projects

Optimization is something I've thought about but haven't done much with for my own image format (from this project if anyone's unfamiliar), so it's neat to see someone else looking into this! The furthest I got was implementing a simple compression format in this project, but unfortunately I don't know a whole lot about compression in general (much less image compression specifically)

I don't think I agree about commas being redundant – if there's only a few of them like in the example, that seems pretty negligible, and they make it simpler to read for both programs and people. I'd personally be more concerned about the length of the part names, but that's probably not important either compared to large images.
Arctenik
Scratcher
18 posts

Developing an image file format suitable for scratch projects

Some thoughts (particularly on compression strategies):
  • With regard to the goal of making the format easy to implement, it's probably most important to make decoding easy, since some features might be optional for encoders but required for decoders
  • Something along the lines of the LZ77 algorithm that DEFLATE uses should almost certainly be included IMO; it'd be pretty useful for compression and would be pretty easy to decode
  • The other component of DEFLATE, Huffman coding, is more complicated, but it's possible it could be made simple enough? There are also other types of entropy coding, but I'm not sure any of them would be easier
  • Lossless image compression often involves modifying pixel values in a reversible way based on adjacent pixels; I imagine this could be something that might help with compression and not be too difficult to implement
  • As far as I can tell one of the things that's used to make image compression more effective is to have a lot of different strategies available for encoders to use; this might be tricky to include while keeping down the number things that a decoder needs to know how to do, but maybe it would be possible to come up with a smaller number of algorithms for the decoder to implement that will do a variety of different things depending on their parameters?? (This is a bit of a vague idea and I don't know how useful it would be)

While trying to research compression techniques, I also happened to find this image format that claims to have fairly good compression while being simple to implement, so maybe it could provide inspiration: https://qoiformat.org/ (In fact it seems simple enough that it kinda makes me wonder if I've gone a bit over-the-top with the stuff I've been talking/thinking about….. :-P )

Last edited by Arctenik (May 18, 2022 01:28:51)

uwv
Scratcher
1000+ posts

Developing an image file format suitable for scratch projects

you can do per-bit deflate in scratch if you want, note it won't be fast but you can implement all bitwise operations to be able to do it
Arctenik
Scratcher
18 posts

Developing an image file format suitable for scratch projects

uwv wrote:

you can do per-bit deflate in scratch if you want, note it won't be fast but you can implement all bitwise operations to be able to do it
Case in point: https://scratch.mit.edu/projects/206098651/
Not very convenient though :-P
awesome-llama
Scratcher
1000+ posts

Developing an image file format suitable for scratch projects

It's interesting to see all these suggestions.

I made the mistake of asking this whilst I have way too many other things to be doing so I don't want to look too deeply into what everyone has suggested just yet.


completeness wrote:

You can implement the DCT (Discrete Cosine Transform), the algorithm JPEG uses, to lossily compress images.
I forgot to mention this in my original post. I think keeping lossy formats separate from lossless would be best for Scratch and what I want to implement is lossless only. I don't know if lossy compression is needed enough to necessitate its implementation. It's great for photos but for transferring other data around? I don't think so.

completeness wrote:

By the way, you can store 24 bits in two characters with only 4096 different characters in your character set. I'm sure that there are at least 4096 characters that can be copied pretty much universally.
That is true but there might still be risks and a lot of unknowns with it. The forums need to support those characters (remember that scratch disallows pastebin and other places to share text). I guess trying to store everything in characters from supported languages could work? I think staying away from this would be best though I do like the idea of it, maybe trying this as a separate thing is better.


Arctenik wrote:

Some thoughts (particularly on compression strategies):

  • As far as I can tell one of the things that's used to make image compression more effective is to have a lot of different strategies available for encoders to use; this might be tricky to include while keeping down the number things that a decoder needs to know how to do, but maybe it would be possible to come up with a smaller number of algorithms for the decoder to implement that will do a variety of different things depending on their parameters?? (This is a bit of a vague idea and I don't know how useful it would be)
These are some good points. I want to highlight the last one, filtering is one method used to aid in compression, and for png, it is done per row. It's not the same as employing different compression techniques but it has worked fine for png with deflate running over it.

Here are png's types (A, B, and C are each neighbouring pixels):
  • None - Zero (so that the raw byte value passes through unaltered)
  • Sub - Byte A (to the left)
  • Up - Byte B (above)
  • Average - Mean of bytes A and B, rounded down
  • Paeth - A, B, or C, whichever is closest to p = A + B − C

I have doubts that doing so for different compression techniques will work out that well. The biggest concern is this would require a much more complicated encoder/decoder that has to have all techniques implemented.
Wikipedia doesn't give much of a list for compression here (https://en.wikipedia.org/wiki/Image_compression)
  • Run-length encoding – used in default method in PCX and as one of possible in BMP, TGA, TIFF
  • Area image compression
  • Predictive coding – used in DPCM
  • Entropy encoding – the two most common entropy encoding techniques are arithmetic coding and Huffman coding
  • Adaptive dictionary algorithms such as LZW – used in GIF and TIFF
  • DEFLATE – used in PNG, MNG, and TIFF
  • Chain codes

Last edited by awesome-llama (May 18, 2022 14:07:52)

DogCatPuppyLover
Scratcher
100+ posts

Developing an image file format suitable for scratch projects

This is lossy compression, but what about a list of lines, their colors, and their sizes? This could be very easily rendered with Scratch pen. The hard part would be encoding into this format. https://scratch.mit.edu/projects/563021622/ Or, you could use quadtrees: https://scratch.mit.edu/projects/564349334/

Last edited by DogCatPuppyLover (May 18, 2022 14:07:19)

Steve0Greatness
Scratcher
1000+ posts

Developing an image file format suitable for scratch projects

crabraveprogrammer wrote:

Don't use commas to separate things. You have so many extra characters, that you can have multiple separation characters, allowing you to sneak in extra information.
But commas just make sense…
Arctenik
Scratcher
18 posts

Developing an image file format suitable for scratch projects

I've been working on implementing an experimental format inspired by the QOI format I mentioned previously, and the strategies used there seem promising! Run-length encoding, specifying pixels by a difference from the previous pixel, and caching recent colors all seem to combine to create fairly effective compression. (At some point in the future I will hopefully have a project demonstrating some different variations of this)
Arctenik
Scratcher
18 posts

Developing an image file format suitable for scratch projects

Here's my experimental project: https://scratch.mit.edu/projects/693020370/

And some thoughts about those strategies:
  • Having a cache of colors ordered by when they were last used seems pretty useful (more effective than a simple hash-based cache, though a little bit slower; ofc there may also be other cache models that could be experimented with). It's also useful to have small cache indices be referenced with shorter character sequences
  • (Run-length encoding is pretty obviously useful for certain images)
  • I'm not really sure what would be the best way to utilize the strategy of representing pixels by a difference from the previous pixel, but it seems like, as with the cache indices, it may be useful to have a short-length-small-value option as well as a larger option
awesome-llama
Scratcher
1000+ posts

Developing an image file format suitable for scratch projects

I've returned to this topic again because I decided to experiment with a little bit of a new format. I'm liking what QOI is capable of so this experiment is also going in that direction.

What I first tried was compressing 1 8-bit channel using a colour table, and an option for using the previous pixel or create a new one, basically what QOI does but without any based on differences. While it did compress, it wasn't great. I was using my image editor example image (the 100*60 tropical beach photo) and did it in about 12k characters. Assuming 3 channels of the same size, that's 36k which does beat CPD (the format of my image editor) with 47k. For comparison with binary formats png and QOI, they were 11.5kb and 12.3kb. If they were stored as base-64 they would be 33% larger according to wikipedia so about 15-16k characters is the target for an alternative text-based format. But also to consider, storing the same image without compression at all except base-64 only takes 24k characters meaning my first attempt and CPD both are worse. CPD is bad because it only uses hexadecimal numbers and the alpha channel is not efficiently compressed.

What made my first test not as good as it could is that due to using 2 base-64 characters per pixel and each pixel only being up to a value of 256, there were a lot of permutations wasted as no colour values could use them.
Now I'm on my second version where there is an option for combining multiple pixels to fill the space. What I currently have it set to is 3 pixels which fits perfectly in 4 base-64 characters. Base-64 isn't actually necessary, though. I should be able to add more characters just by adding more costumes and adjusting a variable that stores the count. I'm not sure if it would help with compression much further, though.

How is the 2nd version doing? 16.3k characters. I haven't tested decompression, though, so there's a chance I've made a mistake somewhere and it's not correct. I hope this is not too good to be true.
Edit: yeah, it was a mistake, I was compressing the wrong pixels (I was supposed to increment a counter to go to the next pixel in a group but I didn't)

I'll paste it here:
experimental_qoi_for_scratch_DO_NOT_USE,v:0.2,size:100,60,symbolcount:64,pxperms:256,pxgroupcount:3,pxgrouplen:4,tablelen:1,maxrepeat:1,pxdir:+x+y,channels:rgb,data:wsLCs7OzsrKyuLi4ubm5t7e3vLy8wMDAxsbGzs7O1NTU19fX4uLi4+Pj5+fn5eXl39/f#X1dXV#U3Nzc#XycnJ#OyMjI~xcXFxMTEu7u7tbW1#I8PDw8/Pz6urq#y~sbGxurq6wMDA#U0tLS3d3d#f#c#f#V#X0dHR#X#O#X2NjYz8/P~ysrKx8fH#Ew8PD#E#F#C#8tra2#4#15ubm8fHxtLS0s7Oz#3#D#7y8vLzc3N#FzMzM09PT~0NDQ#c2dnZ#R#H#P#O#S#R#Fv7+/#FwMDA#E#N#G#/r6+v#6jIyMm5ub6OjosLCwqamp#/#2qqqq#5#R#T#V~#N#T#L#N~#Q#K#H#L#C#D#I#F#G#F#IzMzM#5n5+fk5OTampqgICAmpqa1tbWpKSkoKCgqqqqpqam#aoqKi#C#O#P#N~#E#CwMDA#Fvb29#8#7~#E#G#D#H#I#S#2#Hj4+Pe3t7V1dXc3Nzh4eHpaWl#i#g~#f#kra2t#5rq6us7Ozu7u7#y#z#3#y#t#0#y#7#8#CwcHB#E#J#K#U#c#Ul5eXeHh4b29vfn5+#PmZmZ#ilpaWoaGh#f#l#0#w~#y#z#2r6+v#u~rKys~#0#1vr6+#9#D#G#F2tra#n5ubm#F#WdHR0kpKSiIiI#H#WkJCQ#b#Z#b#gsbGx#y#2#6#x#3#wq6ur#q#t~#z#5tLS0#+#G#+#u29vb#ax8fHjY2N~f39/gYGBiYmJ#TlZWVhYWFmpqa~pqam#t#v!C#u#p#q#r#y#z#r#s#p#y#9#KxcXF#h#709PTwcHB#uk5OT#XfHx8hYWFhoaG#/fn5+gYGB#h#puLi4#6#1~#u#q#lqKio#l#tp6eno6Oj#l#qwMDA#M~#L#E0tLS#u#m#X#Pjo6O#8#I#Bg4OD#FgoKCgICA#yvLy8v7+/wcHBwMDA#7#x#r#n#w#v#z#m#vw8PD#S#dvr6+#H#5nJyc#V#IhISEfn5+enp6d3d3#X#V#T#I#F#6#1#5#9#BwMDAvr6+#8#7#x#rt7e3#+#5wMDAycnJ3t7ewsLC#5#r#Xi4uL#WmJiYlJSUkZGRiYmJ#LgoKCjIyM#C#Ef39/eHh4#0#3uLi4#9#5#4~v7+/xMTEwsLC#D#2~zc3N#3#B#l#cm5ub#OioqKjY2N#X#R~#O#J#MgYGBhISEeXl5gICAfHx8dXV1#s#0#3#C#H#y#2ubm5#0#r#0zc3N#CvLy8#h#Y#V#T#R#K#U#K~#JjY2N#KdHR0aGhob29v#0dnZ2#vZ2dn2dnZ29vb3Nzc4eHh~1NTU#z#u#q#m2tra0NDQ#x#Y#M#NkpKS#R#F#N#I#Fg4OD#K#L#DgoKCaWlpampqf39/eXl5#pbm5u4+Pj~#h#e39/f4uLizc3N#rqqqqyMjI#i#IkJCQ~#F#K#Q#O#R#P#Q#M#TjY2NiIiIfHx8#6#C#0fn5+#uX19fZmZmZWVl#h#j#h#i5eXl39/fr6+vubm509PT#ezc3N#z#3#7nJycmpqam5ub~jY2N#K#I#M#B#+eHh4c3Nzb29vbGxs#2bW1tXl5ea2trVVVV#f#i5OTk5ubm#Zw8PDwMDAy8vL~xsbG19fX#LwcHBzs7OtbW1#7pqam#g#Pg4OD#/e3t7#8cHBw~ampqZGRkYGBgZmZmYWFh#gWlpaUVFR#f4eHh29vbwsLCo6Ojvr6+s7OzvLy8urq6w8PD#CxMTEqqqqpqam#0#Y#cm5ub#c#eampqenp6#weXl5#0XFxcVlZWX19f#k~UlJS#kWVlZW1tb#8rKysoaGhpaWlra2tqampp6enr6+voqKi#l#im5ub#mnJycqKio#m#c#lfn5+#2bW1t#g~XFxc#V#SYWFhYmJiW1tb#h#Z#i#era2t~#zsLCw#jmpqam5ubi4uL#jnZ2dmZmZ#a#6kZGRhISE#P#7d3d3cHBwaWlpZ2dn#wbW1t#i#t#u#rc3Nz#nbGxsW1tbWVlZZWVl#doKCgn5+f#N#4#2XV1d#zhoaG#QlJSUgYGBaGhoU1NT#bT09PR0dHVFRU#s~#q#nY2Nj#h#j#k~#qb29v#kZmZm#V#S#T#J#6#3#u#m#kh4eHj4+P#E#o#0#mX19f#z#Lk5OTlZWVlJSUo6Oj#/fX19dXV1#z#1#q#u~YGBgWlpaU1NTVVVVTk5OUVFR#w#2#gcXFx#i#9R0dHqqqq#WQ0ND#SQUFBS0tLOTk5SEhINTU1Ozs7gYGB#v#3qKionJyc#Q#GeXl5#s#B#FXFxc#V#ST09P~#v#9RUVFRkZGWFhYfHx8MDAwdXV1#sPz8/TU1NNjY2#HQUFBaGhoHh4eMTEx#t#W#s#p#t#l#n~#m#h#k#iY2NjXl5e#c~#b#iSUlJQkJCTExMUFBQcHBw#s#S#0Ly8vODg4NDQ0#2#I#iMzMz#/#Q#TnZ2dpKSkpaWl#k~o6Oj#l!CoqKioaGhoKCgn5+f~#CNTU1#M#0#/#Q#NZWVl#oJCQk#4Hx8fLCws#kOTk5IiIiISEh#4Ojo6pKSkrq6u#ytbW1r6+v#usbGx~s7Oz#u~#vrKyssLCw#S#J#iNzc3#4Kysr#BcXFxYGBgLS0tGhoaHBwc#DExMTMDAw~#0#2Pj4+a2trqamp#ytLS0sLCwsbGx#y#z~#x#w~#v#w~#IRERELy8vNTU1#i#6#N#WV1dXDw8P#c#fFBQUJSUl#FJycnKCgoQEBAMjIyl5eXra2t#x#z#0#z#0#z~srKy#x#y#x~PDw8IyMjJCQk#4KysrKSkp#6ZWVlX19fERERGxsbDg4OGBgY#i#G#r#k#2#4fn5+#sr6+v#y#z#0#z#y~#z#x#w!CZ2dnHx8fQEBAMjIy#o#k#CUVFR#M#oEhISCAgI#TERERLi4u#pFhYW#7#M#Dqamprq6u#tpqam#v~#x#v~#w#vq6ur#s#wT09PcHBwLS0tHR0dJiYm#5#6#HVlZW#d#UHh4eEBAQMTEx#m#fLi4u#/#Em5ub#qpKSklpaW#bpaWl#v#0ra2t#r~#l~#s#ncXFx#oJSUl#h#mLCwsNDQ0Y2NjGxsb#YDAwM#T#lLS0t#c#o#Jg4ODnZ2drKyspqam#XmJiYoqKi#stbW1~#p#kra2t#vsrKyWlpaZWVl#B#bSkpKJCQk#3Pj4+WFhY#QGhoaDQ0N#RGRkZ#6ICAg#kbW1tmJiY#W#srq6uoaGhm5ub~#ptra2uLi4#2#p#ira2t#1#2e3t7GxsbFhYWKSkpGBgYJSUl#gXl5e#SFxcX#Q~#X#ELS0t#0#Ljo6O#hra2t#r~np6e#mm5ub#v#1#2~paWlqKio#ut7e3#6#HSEhI#NMjIy#c#a#I#W#g#a#RDw8P#kHR0dLi4u#K#O~mpqa#hp6enpKSknJycn5+foKCg#stLS0#2#0sbGx#vrq6uJSUl#JLy8v#E#G#UNDQ0TU1N#vQ0NDNzc3#X#ZJCQk#5ICAgZ2dnlpaWkJCQl5eX#a~mJiY#clZWV#X#W#ir6+v#xp6enpaWl#m#uJSUlNTU1TExMEBAQHBwcISEhUVFRcXFxgYGB#D#0DQ0N#/#D#p#3nZ2d#W#aoaGh#i#f#i#enJyc#dmZmZ#a#d~#e#d#eODg4#l#pJycnHh4eIyMj#3YmJi#O#R#MGxsb#CKysrQEBAPT09paWl#f#dpKSkp6enqamp~#s#t#l#f#c#d#cm5ubnp6e#cJiYmGhoaKioq#ETk5OHBwc#6#ieHh4#i#6QUFB#GKSkp#J#yaWlp#noqKi#l#nq6ur#s#vsrKy#uqamppqam#e#l#h#k~oKCg#PDAwMYWFh#3Ozs7KSkp#7T09Pjo6Oa2trKysr#EMDAwODg4#p#8#k#m#k#m#nqqqq#t#v#y#s#o~#n~#l#qqampVFRU#HXV1dd3d3MzMzLi4u#w#F#OcHBw#zLCwsTExM#5MDAwTU1N#gsLCw#no6Ojq6ur#n#p~rq6u~#t~rKys#q#o#s#uUFBQMjIyW1tb#Z#I#0LS0t#/gYGBhoaGICAg#D#1Ly8vhISEi4uLoaGh~#w#k~#r#o#m#p#sr6+v~#ura2t#r#ssrKy#w#+#/WFhYS0tLJSUlNzc3#F#BYmJi#U#+VlZWkJCQ#p#jpaWl#hnZ2d#l~#n#q#o#p#q#r~#usbGx#yt7e3#yuLi4#9#PNzc3ZGRkUlJSNjY2#9#k#NTk5OLi4u#XlpaWnJyc#m!CoqKipKSk#o#m#n#p#o#q#r~#s#wtLS0tra2#4u7u7#Uampq#3XV1d#+#9#6REREV1dX#M#8#ei4uL#G#j#f~#h#l#m#n#l#o~qqqq#o#p#q#s#v#ys7Ozt7e3ubm5#NKysrdHR0XFxc#JUFBQS0tLaWlp#F#J#t#nmJiYjIyMjY2Nk5OT#k#i#o#k~#mqamp#qq6ur~#q#rrq6u~#t#z#xQUFBNDQ0aGho#DWVlZ#I#HgICAVlZWkZGRtLS0#s#y#nmpqa#e#YnZ2d#f#q#i#eqKio#r#s#t#s#t#o#t~#v#t#IMzMzTExM#F~U1NTY2NjdHR0gICAz8/PwsLCtbW1#3wcHB#1#soKCgjo6OmZmZ#ovb29#v#o#s#v#r#q#v#s#u#y#v!C#LUVFRX19fKSkpOTk5bW1tPT09ZGRk0dHR09PTvr6+#3#B#3vb29#ulpaW#Y#mzc3NwMDA#3#o#2#u#q#1~tLS0#4#3#0s7OzRkZG#JhYWF#IQUFBd3d3UVFRk5OT09PT1NTUysrKw8PD#4#z#s#2#dkpKS#dt7e3x8fHv7+/#q#y#1#wubm5#+#9~vLy8urq6#0NjY2Pz8/#MTU1NQEBAgYGB#N#Z1tbWzs7O~ycnJ#9#x#o~#qk5OT#V#o#C#H#4#s#0tra2#0#+v7+/#+#8#6~#1U1NTLi4uT09PGxsbVVVVOzs7#1zMzM0tLSy8vL#D#+#vra2t#l#t#ZlpaW#e#0yMjIwcHB#yrq6u#v#2#z#5~#6#5#8#3JiYmW1tbJycnMDAw#bPT09goKCwMDAxsbG#Jz8/P#+#y#x#v#on5+f#W#Zo6Oj#6#G#1#upqam#4#2#tp6en~#v#6#8LCwsKioqcnJyLy8vRkZGY2NjYWFhoaGh#m#O1tbW#I#3#1#0rKys#l#Y#a#eqqqq#5r6+vsrKy#n#t#/#5#ro6Oj#i#r#6#BMzMz#bNjY2SUlJVFRU#Vm5ubbW1tnJyc2dnZ2tra#+#1#5s7Oz#o#emZmZ#f#l#xsLCw#0#1#mwsLCxMTE#+#v#n~#1wMDAMzMz#NODg4#7#QZWVlkpKSbm5uf39/zc3N0NDQ#Mtra2#8#5#s#j#f#e#h#2#xvb29#B#y#qycnJ#K#B#8#s#2v7+/S0tL#U#zTk5OHBwc#RampqXFxcjIyMwMDA#J#K#/#6wMDA#w#n#h#f#d#mrq6u#6#Bs7Oz#m#Ky8vL#KxcXF#D#/#D#NUFBQOjo6#4RUVFQUFBVlZW#RgoKCwMDAwsLC#I#H#5#9wMDAqqqq#j#impqa#d#jra2t#8#+#s~2tra#PxcXFxsbG#J~wMDAcHBw#U#TgYGBZ2dnXV1d#6#nbW1t#9wcHB#G#B#8#H#1paWl#j#Z~mpqa#g#m#y#2#1#94eHh2tra#N1NTUzMzM#JAAAAqamplZWVkZGRnJyc~mZmZo6Ojra2tuLi4v7+/yMjI0dHR4eHh5eXl7Ozs6Ojo4uLi2tra1tbW19fX4ODg2dnZx8fHzc3NxcXFxsbG#FxMTEtra2sLCww8PD+/v7/Pz89/f3k5OTkZGRj4+PoqKisrKy#NzMzM#a39/f#X#g#X#a1NTU29vb0NDQ3d3d#b#Q~ysrK#G#DwsLC#D#HwMDA#4#ytbW1#K9fX1#7#TkpKSnp6e#2#w#Ky8vL#/#K#W2NjY1dXV#f4uLi#U#D0dHRzs7O09PT~wcHBvr6+#B#+#E#O#HvLy8rq6uwMDA#4#I#1#SjY2NsbGxqKio#cu7u7#W#a#b#a#Q#X#Lzc3N#Oz8/P#I#GycnJwMDA#C#H#E#H#F#J#KtLS0oqKirKysq6urvb29#G7OzsjY2NkJCQnZ2dn5+flpaWmJiY#C#R#P#O~#E#/~#F#7#9~#7#G#J#C#H#F#U#w#J#tqqqqrKys#t#w#Ck5OT~l5eX#Y#epaWlurq6#pr6+v#4#s#xtbW1#upqam#w#y#8~#D!D#F19fX5OTk#hubm5#r#s#ut7e3#/i4uLiYmJm5ublZWVmZmZ#w#r~#s#w~#o#m#joaGh#l#w#y#9~#D#8#y#a7e3t8fHx29vb#1#v#5#0#2#/#9#Ljo6Oj4+P~#p#osbGx#0#m#x#poKCg#f#lpKSk#v#2s7Oz#5#4p6en#O2dnZ3t7e#b#0#2#x#0#5#/#C#6hoaGjIyMl5eX#W#cnp6e#i#e#W#c#i#pra2t#g!C#t#2#9#1hISE#dzs7O1dXV#K#4#+#x#4#6#3#1#5ioqK#Q#g#dmZmZ#f#Y#X#Q#Y#W#f#ZlJSU#W#f#1wMDA#/#D#13Nzc#H#F#8#6~#z#8#6#7#8#6#8#X#g#j#l#n#k#Z#W#U#m#l#n#U#k#5#I2dnZ#w#C#F#7#5#z#y~#z#1xMTE#D~#/wMDA#5#YnJyc#f#k#m#l~#q#k#e#w#4#x#3#C4eHh#B#FwMDA#4#1#7#+#8!C#9#6#/#7#9#7#5#T#Ympqa#dmZmZ#X#a#n#z#1#0#m#qzc3N#qysrK#7#5#8#3#1#3#9#8~#9#6#9#6#9#4#8#6#5jo6OlZWV#a#m#r#T#Z#gm5ub#S#p#N#B#D#2#1#4#5~#3#9#5!C#9#8#0#w#0#3#5#0#z#F#H~y8vL~#+#T#Pi4uLjY2N3t7e#R#5#r#n#u#3#5#0#6~#4#2#6#9#6#5#w#x#6#7#0#3zc3N~y8vL#K#LzMzM#zjY2N#O#84+Pj#K#b#g#f#q#2#3#7~#9#7#+#8#6#3#2#6#2#6#0#u#v#w#L#M~zc3N0NDQycnJ#T#f#L29vb#M#/#Fz8/P#3#6#8~#2#0#1#2#v#x#t#q#r#n#t#o#g#o#Z#H#L#P#R#C#n#o#6#J#K3d3d2dnZ#P#e#E#L#7#5#roaGh#inZ2d#ai4uL#T#amZmZ~#i#g#f#g#Y#Iy8vLxsbG#nioqK#u#c#x#3#F#G#I#o#daGho#KjY2Ni4uLj4+PU1NTYmJib29vhoaGm5ub#m#UkJCQ#W#b#c#S#g#X#b#g#OgoKCgYGB#Lh4eH#K#X#O#SkZGR#PoqKi#a#p#fk5OT#ibW1tfn5+#T#c#i#hnp6e#b#g~#f#i#e#io6Oj#V#W#b#QfHx8cnJye3t7c3Nz#U#V#R#VeXl5#l#is7Oz#n#sqKio#l#n#r~#n#r~#s#r#o#s#l#j#q#BhISEg4ODdHR0aGhoYmJiVVVVZGRk#8#+jIyMiYmJiIiI#O#Z#R#N#W#m#l~#h#e~#d#h#g#m#ppKSk#m#e~#g#t#vaWlpYWFhX19fW1tbgICA#GdXV1U1NT#hV1dXVlZW#igICA#P#d#n#/#k~n5+f#goaGh#e#h#km5ublpaW!Dl5eXXV1dYGBgVFRUYWFh#VcXFxR0dH#eTk5OQkJC#HOTk5Pz8/NjY2QEBAODg4#5#l#f#lx8fHubm5tLS0#wqKiooaGh#wtbW1#Z#Y#W~#X#dY2NjNDQ0PDw8SEhI#tLS0tampqWlpaQUFBUlJSNTU1#B#8#UHx8fMTExfHx8#q#Dk5OT#e!Cn5+fnZ2d#e#h~o6Oj#f!D#aPj4+#4Ozs7#+XV1dW1tbRkZGaGho#0~QEBAOTk5Q0ND#b#2#BV1dX#Uw8PDysrKzMzMy8vL#Mzc3N~zs7O!GY2NjPDw8Q0NDRUVFPT09#B#/#aWVlZJSUl#7#x#tMDAwT09P#8MjIy#dUVFRxsbG0dHR1NTU1tbW#U1dXV#W19fX!C2NjY~#X#Y#pTExM#wRERE#B#x#FX19fVlZW#xKioq~#BISEh#P#SVFRUUFBQYWFhjY2NzMzM09PT#V1NTU#V1tbW~#X#Y#X!EUVFRVFRU#S~#7R0dHVVVV#Q#gHx8f#2IyMj#l#8#o#/RkZGXFxc#bvLy80NDQ#T1NTU1dXV~#W#X#Y~#X#W#X#Y#7#0#BVFRUPDw8#9V1dXdHR0cHBwFhYW#+EhISJycn#+#oTk5O#D#UXl5ep6enzs7O0tLS1NTU!C#V1tbW~19fX#W!Da2trKSkpUFBQ#/#E#7#c#oZmZm#4JycnDw8PHR0dJiYmVlZW#ROjo6X19fbW1t#w#L#S0dHRzc3N#S#T#U~#V!C#T#U1tbWVVVVeXl5LS0tNDQ0#/UVFR#bWFhY#iOTk5MDAwLCwsGhoaTExMTk5O#7TU1NZWVldnZ2wMDAzc3NycnJwMDAxcXFzMzM#U#W#T#S0dHR#Nz8/P#UZGRkeHh4ISEh#w#+#9#VTU1Ng4OD#1#6FxcXGxsb#GU1NT#9#HcnJyq6urwsLCzs7O#JwMDA#C#J#R19fX2NjY#R#M1dXV~#Y#odXV1QEBA#b#Q#xS0tL#eenp6#sMzMzFRUVFxcXNTU1Z2dnRUVFQ0NDkZGRvb29u7u7#O0NDQx8fHxcXFw8PD#P19fX2dnZ#Y#Q#M#S#Z29vbgICA#dHh4eJycnFhYWQEBAOjo6aWlp#e#sGxsb#a#wcXFxV1dXWlpadXV1s7Oz#F#Ozc3N~#Fy8vL#D#U#Y!C#N#P#U#ZS0tL#GQkJCEhISLy8v#0MTExbW1t#m#0#qKCgoKSkpTExM#GVFRUc3Nzs7Oztra2v7+/xsbGy8vLyMjI#D#G#I#Q1tbW#Y1dXV1NTU0tLS~MzMzSkpK#sVFRUQ0ND#dRUVFaWlpPT09UlJST09PKysr#qSUlJWVlZ#9i4uLurq6#2#7vr6+wcHBwsLCw8PD#/#D~ysrK0dHR09PT#NzMzM#O0tLS#xMjIy#JGBgY#e#y#tenp6gICAXV1dUVFR#xbGxsY2NjR0dHXl5e#/#7#+xcXFx8fH#G#H#FxMTE#H#F~#H#I!CycnJRUVF#mKSkp#nJCQk#zTk5OampqiIiIYWFh#qOzs7#sU1NTaGhoZGRk#H#D~yMjIy8vL#M~0dHR#Q#L#I#G#I#G#H#I#H#zIiIi#rQUFB#OKCgo#FYmJifHx8dnZ2W1tbZmZmcHBw#fb29v#XkpKS#J#H#J#Kz8/P#Q09PT1NTU#Rzs7O#L#I#M#J#M#L#JERER~#bSUlJ#9Li4uRERE#XkJCQfX19TExMcnJy#b#hSkpK#t#IysrKycnJ#LzMzM0NDQ#T#U~#P#O~#N!C#P#O#aOjo6#mg4OD#7MjIy#0TU1NlpaW#D#g#eeXl5#l#bgICA#G0tLS#L#K#Pzc3N#Q#P#S~0dHR~#S#P#N#S~#bNjY2bm5upKSkS0tL#z#y#FjY2NlZWV#Le3t7VlZWVFRUrq6utra2#G#I#T#K~#R#P#O~#S#T~1NTU#T#S~1tbW~U1NTUVFR#q#hJiYmNjY2#d#D#4enp6aGhogICAtra2y8vL#H#J#I#H#M~#O#S#P~#Q~0dHR09PT1dXV#W2dnZ#W2traWlpa#h#XcnJySkpKPT09#b#8#dcXFxTExMwcHB~w8PD#J#LzMzMysrK#M#O~#P#Q~#R#S#R#S#U#W2NjY#Z29vb#c#7UFBQdnZ2NjY2T09PSEhIZmZm#i#t#hxMTE#3tbW1yMjI#G#I#J#Lzc3Nz8/P~0NDQ#R!C#Q#R~#U#V#W#Z2tra#jTExMioqK#q#d#l#mfn5+#q#m#L#J#/ubm5u7u7#/zMzMysrK#O#N#P~#R!F#T#S#T#W#VSUlJUVFRgoKC#iUlJSW1tb#jjo6OdHR0#u#Q#L#Q#L#E#HxcXF#I#K0dHRzs7O#M#Q0tLS~#T#S#T#Q#S~#T~UlJSPz8/#m#gQUFB#da2tr#Cmpqa5OTk2tra0tLS#T#a#U#QycnJwcHB#I#R29vb#T#Q#T#U#T#S#U#S#T#V#T#U~VlZWaWlpdXV1Pj4+#Xe3t7#cgICA5eXl6Ojo#Z#U3Nzc#U#a#S#G#H#Q#l3t7e#a#S#a#V#T#Y~19fX#a#Z#X~UFBQ#fmZmZ#f#WhYWF#mpqam5+fn#o4uLi3d3d#V#T#O1tbW#I#E#L2dnZ#i#e#T#X#Y#W#a#d!C#c#b#YSUlJXl5eWVlZ#gQUFBlpaW#e#u6enp#k#l#i2dnZ#S#N#P#R#E#H#S4ODg4+Pj#a#U#Y#Z~3t7e~#d~#a#b#Z#qQkJCZGRkJSUlZ2dnT09P#L4eHh#o#j#e#b#S#R#N#T#I~#M#a#j39/f#X1tbW#U#Z#Y#b#c#b~#c#ZREREdnZ2PDw8SkpKWlpa~nJyc#S#g#j5+fn3Nzc#V#U#S0NDQ#L#I#Lz8/P#d#i#Z#V#R2tra~#U#Q~#V#b#dTExMS0tL#0VFRU#JfX19#ysrKywsLC5eXl6urq#h#Y#X#W#T#QysrKy8vLzMzM#T#c#W#Y#Q#V#d#b#S#O~1NTU#c#fVVVV#wSEhIZWVl#v~r6+viYmJuLi47e3t7Ozs#d#X#a#W#S#M#K#N#R#X~#Z~#Q#g#h#d1dXV#Q#R#Y#fU1NTampq#IWVlZZmZmd3d3pqamhoaGnJyc5OTk#o#k2dnZ3Nzc#b#U#Q#N#M#O#b#Y#e#g#W09PT#k~#f#c#U#Z#fYmJibGxsTk5O#r#+bW1tc3Nz#2pKSk#e#j5eXl#e#b#f#W#S#Q#N#M#R#W#b#f#Y#S#l5OTk#l#k#g#f#h#nUFBQWlpaV1dXaWlpZGRkcnJyYGBgoqKi#e#f5OTk4uLi#b#e4ODg#U0NDQ~#M~#P#V#e#d#V~7u7u#n#j#k!C#f#3ZmZmZ2dnnp6e#F#6WFhYgYGBhISE#d#g#i#g3t7e#l2tra#R#Q#L#M#Lzs7O#R2NjY#a#Z#f8PDw7e3t5ubm6+vr#m#kAAAAj4+PdXV1b29vgoKCfn5+fHx8jo6Onp6era2turq6xMTE0dHR5eXl6+vr8fHx8PDw5+fn4eHh29vb3t7e6Ojo4ODgysrK#RyMjIy8vLzc3N#LtLS0qqqqw8PD/v7+////#+cXFx#vbW1t#OpqamzMzMycnJ#b#h1tbW4uLi#b3Nzc~5OTk19fX5ubm~2NjY#X0NDQ#L#J#L#Nz8/PxsbGvLy8ra2tsLCw#M/Pz8#/cHBw#xh4eHrKysqamp#K#Nubm5#L3d3d#c#e#m6enp#WwsLC#W1NTU#X2trax8fH#D#KxcXFzs7O#Y#OwcHBpaWlvr6+s7Oz#U/f39cnJy#w#lm5ubk5OTwMDA#d#h#k#e09PT#g#P#Q~#T#J#I#Q#D#I#R#P#T#P0tLS#Pr6+vj4+Pl5eXoKCg#C#K9vb2d3d3hISEkpKSmZmZlpaWk5OT#F#U#O~#U#K#B~#Iv7+/#H#G#D#R1dXV#O#R#O#d#zz8/P#ZmJiYpqamoaGhsbGx#QhoaGi4uL#T#Ynp6en5+fvLy8#mq6uru7u7qKio#zt7e3rq6uo6Oj#x#3#H#F#P#O#N#K#I4+Pj8PDw7OzstbW1#ZpKSkqampwMDA#QeHh4h4eH#Xj4+P#S#tp6en#l#p#v#x#n#l#gnJyc#ktra2#5#I#Fy8vLvb29#o39/f9fX1+vr6#j#3#o#+#8#/#Q#OgICAioqKiYmJ#G#k#grKyssrKy#h#v#o#bmpqa#h#gsLCw#+#5#8#y#Ze3t73t7e4eHh#i#x#3#t#zwMDA#N09PTx8fHfHx8g4OD#Jh4eH#SlZWV#Y#W#H#Vnp6e#p~#b#a#c#v#3uLi4#udnZ2jIyM#I#f1tbWwMDA#L#x#+xMTEwMDA#B#I#2#8i4uL#MhISE#M#J#Lfn5+#P#M#clJSUjY2Njo6O#b#xurq6~vr6+#y5eXlzs7Oz8/P#F#B#Ftra2#F#Bx8fHysrK~zc3NgICAiIiIjY2NkZGR#V~#L#J#GoqKio6Oj#m#M#g#2xsbG2dnZ#j#6ycnJ#Hw8PDvLy8u7u7#2~#41dXV~#W#T~#OfX19g4ODhoaG#MkJCQ#SlZWVoaGh#a~#y#6#utbW1w8PD5ubmwMDA#J#H#DwMDAy8vL#P#O~#J#P#K#T0NDQ1NTU#T0dHRcnJyeXl5#9f39/e3t7~#/lJSU#r#v#x#jpqam#T#o0tLS#C#F#L#D#ByMjI#R#Q#S#R#Q1NTU#P#S#P1dXV#U#S#yd3d3gICAjo6OkpKSc3Nz#5h4eHioqK#E#n0tLS#FxsbG#6vb29#D#F#Jx8fH#PysrKzc3N~#S#R#G#B#Jzs7O#Q#P#L#usrKy#1t7e3~#rbm5u~cnJyfX195+fn19fXv7+/#v#r#0#C#G#B#L#J~#H#P#R#O~xMTE#I#T#V#N#S#1~#2~#1ubm5#c#yenp6#16urq#T#b#knZ2drq6u#8#D#L#N#P#N#T#P~#K#N#SzMzM#U#O#I#N#Ps7Oz#0#2#5#8#2fHx8jY2N#J3d3d#O#Hzc3N3Nzc#+#B#I#M#H!C#N#G#J#I#F#I#J#O#M#H#N#B#w#0#5vb29#rjo6OlJSU#u#J#K4uLi5OTk#X6enp#R#W#H#K#/#3#5#4#1pKSk#o#5#+vLy8#G#F#G~#/#v#z#xkpKSd3d3#kjIyM#u#0#G#L#Pqamp#hY2NjhISEh4eHi4uLj4+PU1NTZ2dn#7mZmZ#2#/#0#2#+#DxMTEurq6#I#E#G#HeHh4b29vZWVla2traWlpeXl5hYWF~jY2N#MioqK#knZ2d#0oqKi#Zq6ura2tr#Lp6enu7u7#/wMDA#9#7#ExcXF#C#G#D#J#IgoKChYWFhoaG#y#TS0tLW1tbYmJiiIiI#P#Nk5OT#y#oqqqqxcXF#6wMDAxsbGwsLC#D#J~#Fy8vLzc3Nzs7Oz8/PzMzM#P#L#J#QcHBw~bm5uYWFhVVVVUFBQSkpK#b#u#y#5dHR0e3t7goKClZWVl5eX#a#q#BwsLC~#/#+#B#C#GyMjI#M#P#L#P#Fx8fHysrKV1dX#jX19fVFRU#Q~#5dnZ2#jRkZGUlJSTk5OUVFRWVlZ#7#To6Oj#62NjYwMDA#DwMDA#B#CwMDAxsbG#I#D#CwMDA#D#C#D#OTU1NSEhI#QR0dH#pRkZGkpKSPz8/~RERENzc3PT09ODg4QUFBPDw8QkJCv7+/vLy8wcHB2trazs7O#P#O#JxsbG0NDQ#W#D!CxMTE#DTk5ORkZGJiYmLS0t#4#iKSkpYGBg#RPj4+T09PMTExQEBAOjo6#R#m#3ioqKfX19mZmZ#z#F#Ex8fHyMjI#H~ysrK#Mzs7O#L#K#M#K#pLy8v#pLCws#x#R#P#3WVlZ#tMzMzKioq#zQEBASUlJNDQ0QEBA#g#b4+Pj6Ojo6urq~6+vr7Ozs7u7u#s#u!C8PDw7+/v#uCwsLGhoaLCwsFxcXHh4e#xMDAw#J#RIiIi#tISEh#xJSUl#m#tLy8vR0dH#J5eXl7e3t#u8fHx8vLy#x8/Pz~9fX19PT0~#1~9vb2EhISKCgoERERIyMj#LFhYW#hUVFR#JJSUlHBwc#a#GGRkZ#p#j#6MTEx#Um5ub6enp#t8PDw~8fHx#y#z#0#1#2#1~#2#1#s#v#LBwcHCgoKGxsb#jNDQ0KSkp#X#c#l#a#p#bMDAwMjIyUFBQ#U09PT#r#u8PDw8vLy#x#z9PT0~#1#0#2#0#1#cFBQUCQkJ#HDQ0NERERDAwM#hExMT#U#eGBgYHx8fMDAw#hLi4uNDQ0NTU1WFhYtLS0#r#t7+/v#x!C#y#z~9PT0!DPDw8#S#e#cCAgIEBAQ#H#R#SICAg#c#U#c#gKysr#lLy8v#+b29vZGRk6Ojo#t7u7u#t8PDw~#x~#y#0#y#z~#0#N#h#W#JDg4O#W#N#b#h#c#aKioq#bNjY2#h#sOTk5REREYmJi4uLi6urq~5eXl6enp#t#y#z#y~#x7+/v~#z#5MDAw#U#S#L#Q#J#U#aHR0d#fGBgY#fKCgoJCQk~#6U1NTwMDA#i7Ozs#q#l6Ojo#s8PDw#y#0#w#u#z9fX1~#e#U#3ExMT#6#Q#Y#U#g#c#h#U#YJycn#f#n#4kZGR39/f3t7e#q#t#o#p#o#v#y#0#1#w#v#z9vb2~VFRUERER~IiIiDw8P#N#S#r#X#h#Z#a#hKSkpKioq#6c3Nz2dnZ#l#o#s#t6enp#t#p#x#0#2#1#t#v8/Pz#1JSUlPT09#C#MKCgo#P#Q#SHh4e#l~#d#i#r#m#6Z2dn#Z2tra4eHh5+fn6urq6+vr6Ojo#q#r#v#0!D#z#y#X#5Jycn#bQEBAFBQU#XFRUV#d#3NDQ0#j#g#mKioq#4iIiI3Nzc29vb#f4uLi5OTk5ubm5+fn5eXl#n#p#s#x~#v~#w#xGRkZMDAwNjY2#O#Y#Q#V#0MjIy#0MTExJiYmLS0tLCwsNTU1#+4ODg3d3d#g5ubm#o~#p!C6urq#p#q#r~7Ozs~#rIiIiGhoaISEh#j#Y#VGxsb#w#G#2~Kioq#2Hx8f#tOzs74+Pj#k#l#o#r7e3t~8PDw~#u#s#r#s6urq#s#r#q#Z#SJCQkMzMz#G#UHBwcKSkpKysr#5Ly8v#5#yICAg#x#4jIyM#o~6+vr#s#u#w8vLy8/Pz#y#w#u#t7+/v#u#v#u#s#NCAgIQUFBKysrMDAw#Z#eJycnMzMzPz8/#iLy8vJSUl#x#z#7#o6+vr#q#s#t8PDw#y8/Pz9fX1#y7+/v#w~#v8fHx#z#yMzMz#YHR0d#G#b#a#cIyMjRUVF#8#i#lLi4u#2NTU1X19f5+fn#w#t#s#w7u7u#w~9PT08/Pz!C#0#y#x9fX1#0#S#OKioqbm5u#B#c~#j#4Q0NDJiYm#d#4#EqampwsLC#o#r#y#t~#y#w~#x#0!F#19vb2~#d#UMjIyKysr#Y#g#QLy8v#3#j#mQkJC0tLS#o~6+vr6urq#r7u7u#t7+/v8vLy!C#z#y#0#2~9/f3+Pj4#1#4#hLS0t#J#/#G#k#I#QMTExLCwsLy8v5eXl#o#n#r7Ozs7e3t~#u#w7+/v#w#y~#0#1!C#2#3!C#4ODg4NDQ0#K#kLS0t#a#Y#KMjIyNTU1#44eHh~4ODg#q~#r7e3t#v#w8fHx~#z~9PT0!C9fX1~#2~#3!CLy8vBgYGT09P#7ISEh#v#RLi4uNzc3QEBA#q~#l4+Pj5OTk#o#w~#x~8vLy#x#0~#1#0#1!D#z9/f3#2#bJSUl#LBQUFQEBA#Q#VKSkp#PpKSk#r~#t#r6enp#s~7u7u#w#z#y~#0#1~#2#1~#0#z#0!C#mDw8P#lEhISLS0t~Kioq#+e3t7#2#z#u#w#1#x~7e3t#s#t#z+fn5#1#0#1#2!C#1#0#z#0~#1~#h#6NjY2Hx8f#ZSUlJ#qXV1d+Pj4+/v7#z#w#1#z9vb2#y#u~#z/Pz8#5~#1#5#3#1#3#2~#3!D#SKSkpYWFh#BMzMzYGBg#+o6Oj+vr6#7#5#3#x#y#w#07+/v#t#v#3#7#6#1#3#4~#5#6#5~#4~#3ISEh~NjY2#DODg4aWlpPDw8tLS0#7#5~#6#1#y#x~#y#t#v9PT0#5#7#59vb2+Pj4#5#4#6~#5~#4#5#4JCQk#fOjo6#X#EKysrxsbG#2/Pz8#5#4#38/Pz#y#x#z#v~#y#5#8+vr6#4~#3#4#5#6~#5#4#7#5#QR0dHDAwM#hSkpKOzs7j4+P5eXl#5#6+/v7#3#0!C#y#x#w#x#0#6#7#5#3#2#5~#2!C#3#7#6#c#QVlZWKCgoJSUlX19fV1dXxMTEzMzM#7#8#5#2!C#z#0#w#x#z#2#6#2#5#3~#7#5#2#1~#3#6#8BgYGNzc3LS0tOjo6#BUVFRvb29hISE#M#8/f399/f3#2#4#3#0#z#x#0#2#4#3#5~#3+vr6!C#4#3#2#5~#IFxcXLCws#hNzc3WVlZq6urhYWFtbW1#7!C9/f3#4~9fX1#z!C#1#6#4#6#7#5#3#7#8#6#5#3#6#7ICAg#c#bNjY2#T#odnZ2YWFhvb29#5#7#8#6#4#69vb2~#0!C#1#4#5#6#4#2#7#8/f39#7#6#7#6#9MzMz#mFhYW#t#U#+#dtra2#6~#8#7#5#6#7#19vb2#18/Pz#0#2#4#7~#3~#9#8#6#7~#8#6#B#qKSkpaWlp#K#BOjo6c3NzdXV1#5+vr6#8#6#5#9#6#39fX18/Pz#0#z#1#3#4#5~#7#9~#8#9#8~AAAA

I will document it properly later but things to note are:
  • colour characters are the same as Base64 (ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/)
  • ! means RLE with the count immediately after
  • ~ means use previous pixel group exactly once (like RLE but without a count)
  • # means use the table (similar method as QOI but it's just value mod tablelen)

Last edited by awesome-llama (Oct. 24, 2022 04:18:44)

skymover1239
Scratcher
500+ posts

Developing an image file format suitable for scratch projects

This is a good idea! May I suggest something like the following?
color, thickness, x position, y position


#000, 10, 0, 0
#000, 10, 0, 1
#000, 10, 0, 2
or maybe if it's going to be stored into a single list element or variable.
#000, 10, 0, 0;#000, 10, 0, 1;#000, 10, 0, 2
meunspeakable
Scratcher
100+ posts

Developing an image file format suitable for scratch projects

awesome-llama wrote:

~snip~
I read the OP and was just going to mention QOI!
The most used format is just to use a list that is 480x360 items long and pen color values, so a decoder that implemented that would be great.
awesome-llama
Scratcher
1000+ posts

Developing an image file format suitable for scratch projects

skymover1239 wrote:

This is a good idea! May I suggest something like the following?
color, thickness, x position, y position
#000, 10, 0, 0
#000, 10, 0, 1
#000, 10, 0, 2
or maybe if it's going to be stored into a single list element or variable.
#000, 10, 0, 0;#000, 10, 0, 1;#000, 10, 0, 2
This topic is about handling bitmap images, not vector. Your suggestion seems to be closer to a vector format and it also doesn't hold up to the requirement of being compressed as well as the second point of my original post asking for colour channels including alpha.
awesome-llama
Scratcher
1000+ posts

Developing an image file format suitable for scratch projects

meunspeakable wrote:

awesome-llama wrote:

~snip~
I read the OP and was just going to mention QOI!
The most used format is just to use a list that is 480x360 items long and pen color values, so a decoder that implemented that would be great.
The goal is to have an image format that is suitable for most use cases, similar to how png and jpg are used everywhere. They don't have niche uses. Of course that'll mean a 480*360 RGB image will be supported, along with any resolution and hopefully more channels.
meunspeakable
Scratcher
100+ posts

Developing an image file format suitable for scratch projects

awesome-llama wrote:

meunspeakable wrote:

awesome-llama wrote:

~snip~
I read the OP and was just going to mention QOI!
The most used format is just to use a list that is 480x360 items long and pen color values, so a decoder that implemented that would be great.
The goal is to have an image format that is suitable for most use cases, similar to how png and jpg are used everywhere. They don't have niche uses. Of course that'll mean a 480*360 RGB image will be supported, along with any resolution and hopefully more channels.
well, There are some changes that could be made to QOI. Adding some elements of PNG/WebP/JPEG XL Lossless might be helpful as QOI was created by one guy, although he had feedback, he didn't implement a lot of them due to the fact that he wanted it to be “simple” over efficiency and effectiveness. The main change that would be good to implement would be to unrestrict QOI to be byte-based, as most thing in scratch aren't based around binary and could allow for better and faster compression.
meunspeakable
Scratcher
100+ posts

Developing an image file format suitable for scratch projects

meunspeakable wrote:

awesome-llama wrote:

meunspeakable wrote:

awesome-llama wrote:

~snip~
I read the OP and was just going to mention QOI!
The most used format is just to use a list that is 480x360 items long and pen color values, so a decoder that implemented that would be great.
The goal is to have an image format that is suitable for most use cases, similar to how png and jpg are used everywhere. They don't have niche uses. Of course that'll mean a 480*360 RGB image will be supported, along with any resolution and hopefully more channels.
well, There are some changes that could be made to QOI. Adding some elements of PNG/WebP/JPEG XL Lossless might be helpful as QOI was created by one guy, although he had feedback, he didn't implement a lot of them due to the fact that he wanted it to be “simple” over efficiency and effectiveness. The main change that would be good to implement would be to unrestrict QOI to be byte-based, as most thing in scratch aren't based around binary and could allow for better and faster compression.
The biggest necessary change is to replace hashing with a cache.
awesome-llama
Scratcher
1000+ posts

Developing an image file format suitable for scratch projects

meunspeakable wrote:

well, There are some changes that could be made to QOI. Adding some elements of PNG/WebP/JPEG XL Lossless might be helpful as QOI was created by one guy, although he had feedback, he didn't implement a lot of them due to the fact that he wanted it to be “simple” over efficiency and effectiveness. The main change that would be good to implement would be to unrestrict QOI to be byte-based, as most thing in scratch aren't based around binary and could allow for better and faster compression.
These points were already being discussed in the previous posts. If you have specifics to mention, I'd like to hear them.

meunspeakable wrote:

The biggest necessary change is to replace hashing with a cache.
I don't understand, hashing is not used to store images.

Last edited by awesome-llama (Oct. 24, 2022 03:44:29)

awesome-llama
Scratcher
1000+ posts

Developing an image file format suitable for scratch projects

Update to the compression I was testing, it does work along with decompression but it seems to not have managed to compress the test image much unfortunately. There was an issue I missed and too many pixels were being compressed.

Here's a working example of the format, RGB.
pti,v:0.3,x:14,y:17,sc:64,pxp:256,pxc:1,pxl:2,tlen:1,rep:1,dir:+x+y,channels:rgb,data:D/!O#A!L#/~#ADy#/!I#A#/~#A#y#/!I#A#/~#A#y#/!I#A#/~#A#y#A!CAB#A#B#A#/~#A#/~#A#y#/!I#A#/~#A#y#/!I#A#/~#A#y#A#/!D#A#/!C#A#/~#A#yD5#/!H#A#/~#A!L#/!F#A!D#/!F#A!L#/~#AAM#A!J#/~#A#M#A!J#/~#A!L#/!O,D/!O#A!L#/~#ADy#/!I#A#/~#A#y#/!I#A#/~#A#y#/!I#A#/~#A#y#A~ADABAC#A#C#/~#A#/~#A#y#/!I#A#/~#A#y#/!I#A#/~#A#y#A#/!D#A#/!C#A#/~#A#y#/!I#A#/~#A!L#/!F#A!D#/!F#A!L#/~#ADa#/!I#A#/~#A#a#/!I#A#/~#A!L#/!O,D/!O#A!L#/~#AAPAB!I#A#/~#A#P#B!I#A#/~#A#P#B!I#A#/~#A#P#A!DAH#A~#B~#A#/~#A#P#B!I#A#/~#A#PD+#/~#B~#/!C#B#A#/~#A#P#A#/~#B~#A#/~#B#A#/~#A#PD9#/~#B~#/!C#B#A#/~#A!L#/!F#A!D#/!F#A!L#/~#ABQBV!I#A#/~#A#Q#V!I#A#/~#A!L#/!O,

Issues to address:
  • Preceeding data (header?) is quite lengthy and for smaller images it can make it noticably worser than other formats.
  • Frequent table lookups also waste lots of hash characters. Maybe only having it before a set of lookups is better? The length of each group is fixed (tlen=1 for example).
  • Differences between neighbouring pixels are not handled yet

Maybe this format will end up being specialised. It might not solve all the challenges I tried to solve but it's still useful. Will have to see.

In the current state, here are some measurements using test images:
  • [100x60=6000] Beach - 23.8k
  • [64x64=4096] 4-bit - 10k
  • [14x17=238] Appel pixel art - 680
  • [16x16=256] Minecraft grass block - 1k
  • [96x72=6912] NPR guy (downscaled) - 25.3k

Last edited by awesome-llama (Oct. 24, 2022 06:18:43)

Powered by DjangoBB