Instagram Media Previews

Posted

I stumbled across BlurHash recently and thought it was an interesting technique to get pretty previews for a small space overhead. However someone then pointed out that Instagram had its own preview method which is very clever. The only information I could find about it was an answer on Stack Overflow so I thought it would be interesting to look into.

The basic concept is simple. Instagram will send you a low-quality JPEG preview with the API response. This is nothing particularly unique however the custom “compression” that they perform is. They use a common fixed header for all of the preview images and embed that into their application code. Then they include the preview images (minus the header) inline in the API responses.

It starts with the media_preview item in the API response. This is clearly a base64 encoded value. However on it’s own it doesn’t seem to be anything.

00000000: 002a 2a9a 160e 4b39 04e7 014e 381d b1fa  .**...K9...N8...
00000010: 73de a000 994b 480f ca7e 5c7a 03c7 7e94  s....KH..~\z..~.
00000020: 8d24 c48c c431 c019 1d4f e38c fd3f 1a6a  .$...1...O...?.j
00000030: c336 7843 c93c 138c 8eff 00a7 7edc 566d  .6xC.<......~.Vm
00000040: 9562 4bd9 d77a b2fd e5c8 20f6 cf41 e9ef  .bK..z.... ..A..
00000050: 9fa5 5c57 12c6 6218 2369 c641 1dbb 31e0  ..\W..b.#i.A..1.
00000060: 8f7f a1ac e963 9189 1b70 f8c8 1e84 7239  .....c...p....r9
00000070: f703 bf27 d6ad 25eb 4b18 5098 0e3b 71f9  ...'..%.K.P..;q.
00000080: 1c8c ff00 2f6a 57ea 3b74 312d f723 0c75  ..../jW.;t1-.#.u
00000090: 5604 0f7f 5fd2 b677 3777 8f3f 407f 5eff  V..._..w7w.?@.^.
000000a0: 005a c892 37b7 9483 d54f ff00 5feb d3af  .Z..7....O.._...
000000b0: e557 07d9 cf24 9cfd 07f8 d0f5 2907 db1f  .W...$......)...
000000c0: 21f6 fdcf 527f 3ffe bd59 5d50 9c16 503d  !...R.?..Y]P..P=
000000d0: c13f e159 ca8f c0dc 7f4a 0c4c dd1b eb9f  .?.Y.....J.L....
000000e0: ff00 5d1a 13a9 a7f6 b52c 5f71 049c 8c83  ..]......,_q....
000000f0: c0e9 d31d 3eb5 269b 7110 4d8d c104 803f  ....>.&.q.M....?
00000100: 1246 3d3a d652 c720 ee3f 2a6c 2594 b283  .F=:.R. .?*l%...
00000110: 8c37 a67a ff00 2a34 19a5 7cb1 1914 ab67  .7.z..*4..|....g
00000120: 38dc 7aff 009f a550 2541 eff9 8ffe 26a1  8.z....P%A....&.
00000130: b92c a416 e49f 6f4f 6a76 c3e8 691a 2004  .,....oOjv..i. .
00000140: 751f ce93 2474 c0fc 3ffd 7500 3822 9fde  u...$t..?.u.8"..
00000150: 8b0a c23b c883 8355 e376 0fbb bd59 93fd  ...;...U.v...Y..
00000160: 57e2 2a44 5181 c0ed fca9 df42 2da9 0cb2  W.*DQ......B-...
00000170: 330f ae45 2877 c741 f97f f5e8 9fa0 faff  3..E(w.A........
00000180: 004a 894f 028e 807f ffd9                 .J.O......

The magic starts when you merge it with the header data embedded in the Instagram app. I have cleaned up the function a bit and it looks as follows.

const common_header = atob("/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEABsaGikdKUEmJkFCLy8vQkc/Pj4/R0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0cBHSkpNCY0PygoP0c/NT9HR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR//AABEIABQAKgMBIgACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/AA==");

function media_preview_to_url(preview_string) {
	let preview_binary = atob(preview_string).split("");

	let [unused, width, height] = preview_binary.slice(0, 3);
	let data = preview_binary.slice(3);

	let header = common_header.split("");
	header[160] = height;
	header[162] = width;

	let jpeg_data = header.concat(data).join("");
	return "data:image/jpeg;base64," + btoa(jpeg_data);
}

The code treats the first three bytes of the decoded media_preview value specially. The second and third byte are the width and height of the image. These are slotted into the header at the correct postitions. (These bytes are originally set to 0 in the common header data). The first byte is unused and appears to be always 0. My guess is that this is some sort of version identifier. If they ever wish to change some header parameters, switch to a different image format or another backwards incompatible change they can simply bump this number to 1 and the decoder can then use the new implementation.

Sharing the common header saves 809 base64 bytes for each preview string. This turns out to be about 550 bytes compressed. Overall the savings are not enormous but saving more than 50% of the data for previews is quite nice. The tradeoff is that you need to use the same parameters for all of your preview images, I haven’t looked into this closely but I suspect that for super-low-quality images having parameters that aren’t quite ideal is almost negligible.

Results

Here is an example preview vs the full image.

full quality image 1preview 1

Full 127 KiB, Preview 445 B (compressed)

full quality image 2preview 2

Full 115 KiB, Preview 389 B (compressed)

Try it Yourself

Paste a media_preview value into the box below to see the decoded image. You can find this value by opening your browser’s dev tools on instagram.com and watching the network requests as you scroll.

Encoded size: ? bytes.