Back to timeline
hobby 2023

A whole song hidden inside one image

I had built custom encryption before, but never my own compression, and I had spent years hooked on hiding data inside images through Norway's PST advent-calendar CTF. So I put the two together: I took the full ASCII version of a certain song from an earlier project, compressed it with a scheme of my own, and tucked the whole thing into the low bits of a single frog photo. The decompressor below is the real one, ported to run in your browser. It reads the hidden bits straight out of the image and plays the song back as ASCII.

cmd.exe
>  

Nothing is downloaded except the frog photo itself. Every character above is decoded live from the low four bits of that one image, in real time.

The idea

At that point I had written my own encryption for data, but I had never built a compression algorithm, even though it always looked like a fun problem. I was also fascinated by hiding data in plain sight. I had spent about three years doing the PST advent-calendar CTF (NTPS), and that is where I first got into hiding data inside images, where a payload can live in the parts of a picture your eye never notices.

I even had the perfect thing to compress: the ASCII version of an entire song, lifted from an earlier project. The goal was to squeeze the whole thing into one image and to make the decompressor as short as I possibly could.

Hiding the data

Every pixel of the carrier image has four channels: red, green, blue and alpha. Each channel is a byte, and the top four bits carry almost all of the colour your eye sees. The bottom four bits barely change the picture at all, so that is where the payload goes. Four channels times four spare bits gives sixteen bits of hidden storage per pixel, and across a multi-megapixel image that is enough room for the whole song.

The carrier is deliberately a busy photo: a frog, with a lot going on in the texture and lighting. A noisy, detailed image hides the small wobble in the low bits far better than a flat one would, so the data stays invisible.

The compression

The song is mostly the same handful of glyphs repeated in long runs, so the scheme is built around a small dictionary of symbols plus run-length coding, packed into a tight bitstream. Reading it back, each token is:

  • one flag bit, saying whether this token repeats,
  • five bits for the symbol index into the dictionary,
  • and, only if the flag is set, five more bits for a repeat count.

So a single character costs six bits, and a run of up to thirty-two of the same character costs eleven. Decoding walks the image pixel by pixel, peels the sixteen hidden bits out of each one, refills a bit buffer, and emits symbols until it has a full screen: forty-eight rows of one hundred and thirty-one characters, printed at twenty-five frames per second. The result is the song playing as ASCII video, straight out of the frog.

The decompressor

The part I am most happy with is how short the reader and player ended up. The whole thing, opening the image, pulling the bits, expanding the dictionary tokens and animating the frames, is four lines of Python:


      
      

The list A does triple duty: slot zero is the image, the next slots hold the dictionary of glyphs, and the last two are reused as the bit buffer and the output buffer. The terminal at the top of this page runs that exact algorithm, decoded faithfully in JavaScript. Because the data lives in the low bits, the image has to be read losslessly, so the page decodes the PNG itself rather than going through a canvas, which would round the bits off and scramble the stream.

Want to poke at it yourself? Here are both files, the carrier image and the four-line player, zipped together:

Download the image and decompressor (ZIP, 13 MB)

The carrier

And here is the image itself, the one holding the entire song. It looks like an ordinary photo, because that is the whole point.

A busy, detailed photo of a frog, used as the carrier image that secretly holds the compressed song in its low bits.
The frog blesses you with luck in these trying times.
Back to timeline