{"article":{"slug":"each-step-of-decoding-a-png","title":"Each step of decoding a PNG","subtitle":null,"summary":"Smitty van Bodegom walks through the complete process of decoding a PNG file—chunks, zlib decompression, scanline filters and Adam7 interlacing—illustrated with a sample image at every stage.","content_type":"tutorial","language":"en","canonical_url":"https://iter.ca/post/png/","author":{"name":"Smitty van Bodegom","url":"https://iter.ca/","person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"iter.ca","url":"https://iter.ca/","listing_slug":null,"listing":null},"topics":[{"name":"Programming","slug":"programming","url":"https://listedarticles.com/topics/programming"},{"name":"Graphics","slug":"graphics","url":"https://listedarticles.com/topics/graphics"},{"name":"Tutorials","slug":"tutorials","url":"https://listedarticles.com/topics/tutorials"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":1274,"reading_minutes":6,"published_at":"2022-03-11T00:00:00.000Z","added_at":"2026-10-18T02:12:44.942Z","updated_at":"2026-10-18T02:12:44.942Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/each-step-of-decoding-a-png","markdown_url":"https://listedarticles.com/articles/each-step-of-decoding-a-png.md","example":false,"citation":"Smitty van Bodegom, iter.ca. \"Each step of decoding a PNG.\" 11 Mar 2022. https://iter.ca/post/png/ (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://iter.ca/post/png/"},"body_markdown":"Let's look at the complete process of decoding a PNG file. Here's the image I'll be using to demonstrate:\n\n![Several letter 'S's on top of each other, each with a different color.](https://smitop.com/a256adpt.png)\n\n## Chunking\n\nPNG files are stored as a series of chunks. Each chunk has a 4 byte length, 4 byte name, the actual data, and a CRC. Most of the chunks contain metadata about the image, but the `IDAT` chunk contains the actual compressed image data. The capitalization of each letter of the chunk name determines how unexpected chunks should be handled:\n\n| UPPERCASE | lowercase |\n| --- | --- |\n| Critical | Ancillary | Is this chunk required to display the image correctly? |\n| Public | Private | Is this a private, application-specific, chunk? |\n| Reserved | Reserved |  |\n| Unsafe to copy | Safe to copy | Do programs that modify the PNG data need to understand this chunk? |\n\n![c: Ancillary, H: Public, R: Reserved, M: Unsafe to copy](https://smitop.com/Chunk-letters.svg)\n\nHere are the chunks in the image:\n\n Show metadata chunks\n\nSignature\n\nThis identifies the image as a PNG. It contains newline characters and other characters that often get mangled to ensure that the binary image data didn't get corrupted.\n\n89 50 4E 47 0D 0A 1A 0A\n\nHeader\n\nThis contains metadata about the image.\n\nLength 00 00 00 0D = 13 bytes long\n\nType 49 48 44 52 = IHDR\n\nData\n\n 00 00 01 00 00 00 01 00 08 02 00 00 00\n\n|  |  |\n| --- | --- |\n| Width | 256 |\n| Height | 256 |\n| Bit depth | 8 |\n| Color space | Truecolor RGB |\n| Compression method | 0 |\n| Fitler method | 0 |\n| Interlacing | disabled |\n\nCRC D3 10 3F 31\n\nGamma\n\nThis contains the image gamma, used to display more accurate colours.\n\nLength 00 00 00 04 = 4 bytes long\n\nType 67 41 4D 41 = gAMA\n\nData\n\n 00 00 B1 8F\n= 45455\n\nCRC 0B FC 61 05\n\nColor space information\n\nThis contains data about where in the [full CIE color space](https://en.wikipedia.org/wiki/CIE_1931_color_space) the colors in the image are, so that monitors that support colors outside the standard sRGB space can display the image better.\n\nLength 00 00 00 20 = 32 bytes long\n\nType 63 48 52 4D = cHRM\n\nData\n\n 00 00 7A 26 00 00 80 84 00 00 FA 00 00 00 80 E8 00 00 75 30 00 00 EA 60 00 00 3A 98 00 00 17 70\n\nCRC 9C BA 51 3C\n\nPhysical dimensions\n\nThis contains the physical dimensions of the image, so it can be displayed at the right physical size when possible.\n\nLength 00 00 00 09 = 9 bytes long\n\nType 70 48 59 73 = pHYs\n\nData\n\n 00 00 0B 13 00 00 0B 13 01\n2835 pixels = 1 metre\n\nCRC 00 9A 9C 18\n\nLast modification date\n\nThis contains the time the image was created at.\n\nLength 00 00 00 07 = 7 bytes long\n\nType 74 49 4D 45 = tIME\n\nData\n\n 07 E5 05 0A 0A 2F 2C\n2021-05-10, 10:47:44 (UTC)\n\nCRC 00 53 9E DD\n\nBackground color\n\nThis contains the background color of the image to be displayed while the image is loading\n\nLength 00 00 00 06 = 6 bytes long\n\nType 62 4B 47 44 = bKGD\n\nData\n\n 00 FF 00 FF 00 FF\n= white\n\nCRC A0 BD A7 93\n\nImage data\n\nThis contains the compressed image data.\n\nLength 00 00 F5 51 = 62801 bytes long\n\nType 49 44 41 54 = IDAT\n\nData\n\n...\n(the compressed and filtered bytes of the image)\n\nCRC BF EB 1B 15\n\nText\n\nThis can store arbitrary tagged textual data about the image.\n\nLength 00 00 00 27 = 39 bytes long\n\nType 74 45 58 74 = tEXt\n\nData\n\n 46 69 6C 65 00 2F 68 6F 6D 65 2F 73 6D 69 74 2F 50 69 63 74 75 72 65 73 2F 69 63 6F 6E 33 2F 33 64 2E 62 6C 65 6E 64\n= key: \"File\", value: \"/home/smit/Pictures/icon3/3d.blend\"\n\nCRC 88 DA 55 E7\n\nText\n\nThis can store arbitrary tagged textual data about the image.\n\nLength 00 00 00 13 = 19 bytes long\n\nType 74 45 58 74 = tEXt\n\nData\n\n 52 65 6E 64 65 72 54 69 6D 65 00 30 30 3A 31 32 2E 31 35\n= key: \"RenderTime\", value: \"00:12.15\"\n\nCRC 5E 2F 7A B4\n\nTrailer\n\nThis empty chunk indicates the end of the PNG file.\n\nLength00 00 00 00 = 0 bytes long\n\nType49 45 4E 44 = IEND\n\nCRCAE 42 60 82\n\n## Extracting the image data\n\nWe take all of the `IDAT` chunks and concatenate them together. In this image, there is only one `IDAT` chunk. Some images have multiple IDAT chunks, and the contents are concatenated together by the PNG decoder. This is so streaming encoders [don't need to know the total data length up front](https://stackoverflow.com/a/29517059/10113238) since the data length is at the beginning of each chunk. Multiple IDAT chunks are also needed for encoding images with image data having a length longer than the largest possible chunk size (2554) Here's what we get if treat the compressed data as raw image data:\n\n![Random noise on the top third, with the rest black.](https://smitop.com/fmat/png/idat-raw.png)\n\nIt looks like random noise, and that means that the compression algorithm did a good job. Compressed data should look higher-entropy than the lower-entropy data it is encoding. Also, it's less than a third of the height of the actual image: that's some good compression!\n\n## Decompressing\n\nThe most important chunk is the `IDAT` chunk, which contains the actual image data. To get the filtered image data, we concatenate the data in the `IDAT` chunks, and decompress it using zlib. There is no image-specific compression mechanism in play here: just normal zlib compression.\n\n![The example image, increasingly skewed to right from top to bottom. It is mostly black and white, with pixels of intense color scattered throughout.](https://smitop.com/fmat/png/idat.png)\n\nAside from the colors looking all wrong, the image also appears to be skewed horizontally. This is because each line of the image has a filter as the first byte. Filters don't directly reduce the image size, but get it into a form that is more compressible by zlib. I [have written about PNG filters before](https://observablehq.com/@smitop/png-filters).\n\n## Defiltering\n\nWe take the decompressed data and undo the filter on each line. This gets us the decoded image, the same as the original! Here's the popularity of each filter type:\n\nNone 0\n\nSubtract 23\n\nUp 77\n\nAverage 134\n\nPaeth 22\n\n![The example image](https://smitop.com/fmat/png/defiltered.png)\n\n## Interlacing\n\nPNGs can optionally be interlaced, which splits the image into 7 different images, each of which covers a non-overlapping area of the image:\n\n![Diagram of the 7 passes in an 8x8 area. The first is a single pixel in the top-left, and the seventh is every other row.](https://smitop.com/ext/Adam7-mul.svg)\n\nEach of the 7 images is loaded in sequence, adding more detail to the image as it is loaded. This sequence of images is called [Adam7](https://en.wikipedia.org/wiki/Adam7_algorithm).\n\nThe data for each of the 7 images are stored after each other in the image file. If we take the example image and enable interlacing, here's the raw uncompressed image data:\n\n![Same as the earlier skewed letter S, but there are multiple stacked on top of each other. The bottom half has 2, the third above that has 2 more, and above that it increasingly looks like random noise.](https://smitop.com/fmat/png/inter-idat.png)\n\nHere are the 7 passes that we can extract from that image data, which look like downscaled versions of the image:\n\n![](https://smitop.com/fmat/png/inter-1.png)\n![](https://smitop.com/fmat/png/inter-2.png)\n![](https://smitop.com/fmat/png/inter-3.png)\n![](https://smitop.com/fmat/png/inter-4.png)\n![](https://smitop.com/fmat/png/inter-5.png)\n![](https://smitop.com/fmat/png/inter-6.png)\n![](https://smitop.com/fmat/png/inter-7.png)\n\nSince some of those sub-images are rectangles but the actual image is square, there will be more details horizontally than vertically when loading the image, since horizontal detail is more important than vertical detail for human visual perception.\n\n## Bonus: bugs\n\nHere's what you get when you have a bug in the Average filter that causes it to treat overflows incorrectly (the way integer overflow in filter value calculation is specified to be a bit different than the rest):\n\n![The example image, but it looks glitchy starting a quarter of the way down.](https://smitop.com/pngbug.png)\n","body_html":"<p>Let&#39;s look at the complete process of decoding a PNG file. Here&#39;s the image I&#39;ll be using to demonstrate:</p>\n<figure><img src=\"https://smitop.com/a256adpt.png\" alt=\"Several letter &#39;S&#39;s on top of each other, each with a different color.\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<h2 id=\"chunking\">Chunking</h2>\n<p>PNG files are stored as a series of chunks. Each chunk has a 4 byte length, 4 byte name, the actual data, and a CRC. Most of the chunks contain metadata about the image, but the <code>IDAT</code> chunk contains the actual compressed image data. The capitalization of each letter of the chunk name determines how unexpected chunks should be handled:</p>\n<div class=\"table-wrap\"><table><thead><tr><th>UPPERCASE</th><th>lowercase</th></tr></thead><tbody><tr><td>Critical</td><td>Ancillary</td></tr><tr><td>Public</td><td>Private</td></tr><tr><td>Reserved</td><td>Reserved</td></tr><tr><td>Unsafe to copy</td><td>Safe to copy</td></tr></tbody></table></div>\n<figure><img src=\"https://smitop.com/Chunk-letters.svg\" alt=\"c: Ancillary, H: Public, R: Reserved, M: Unsafe to copy\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>Here are the chunks in the image:</p>\n<p> Show metadata chunks</p>\n<p>Signature</p>\n<p>This identifies the image as a PNG. It contains newline characters and other characters that often get mangled to ensure that the binary image data didn&#39;t get corrupted.</p>\n<p>89 50 4E 47 0D 0A 1A 0A</p>\n<p>Header</p>\n<p>This contains metadata about the image.</p>\n<p>Length 00 00 00 0D = 13 bytes long</p>\n<p>Type 49 48 44 52 = IHDR</p>\n<p>Data</p>\n<p> 00 00 01 00 00 00 01 00 08 02 00 00 00</p>\n<div class=\"table-wrap\"><table><thead><tr><th></th><th></th></tr></thead><tbody><tr><td>Width</td><td>256</td></tr><tr><td>Height</td><td>256</td></tr><tr><td>Bit depth</td><td>8</td></tr><tr><td>Color space</td><td>Truecolor RGB</td></tr><tr><td>Compression method</td><td>0</td></tr><tr><td>Fitler method</td><td>0</td></tr><tr><td>Interlacing</td><td>disabled</td></tr></tbody></table></div>\n<p>CRC D3 10 3F 31</p>\n<p>Gamma</p>\n<p>This contains the image gamma, used to display more accurate colours.</p>\n<p>Length 00 00 00 04 = 4 bytes long</p>\n<p>Type 67 41 4D 41 = gAMA</p>\n<p>Data</p>\n<p> 00 00 B1 8F\n= 45455</p>\n<p>CRC 0B FC 61 05</p>\n<p>Color space information</p>\n<p>This contains data about where in the <a href=\"https://en.wikipedia.org/wiki/CIE_1931_color_space\" rel=\"nofollow ugc noopener\">full CIE color space</a> the colors in the image are, so that monitors that support colors outside the standard sRGB space can display the image better.</p>\n<p>Length 00 00 00 20 = 32 bytes long</p>\n<p>Type 63 48 52 4D = cHRM</p>\n<p>Data</p>\n<p> 00 00 7A 26 00 00 80 84 00 00 FA 00 00 00 80 E8 00 00 75 30 00 00 EA 60 00 00 3A 98 00 00 17 70</p>\n<p>CRC 9C BA 51 3C</p>\n<p>Physical dimensions</p>\n<p>This contains the physical dimensions of the image, so it can be displayed at the right physical size when possible.</p>\n<p>Length 00 00 00 09 = 9 bytes long</p>\n<p>Type 70 48 59 73 = pHYs</p>\n<p>Data</p>\n<p> 00 00 0B 13 00 00 0B 13 01\n2835 pixels = 1 metre</p>\n<p>CRC 00 9A 9C 18</p>\n<p>Last modification date</p>\n<p>This contains the time the image was created at.</p>\n<p>Length 00 00 00 07 = 7 bytes long</p>\n<p>Type 74 49 4D 45 = tIME</p>\n<p>Data</p>\n<p> 07 E5 05 0A 0A 2F 2C\n2021-05-10, 10:47:44 (UTC)</p>\n<p>CRC 00 53 9E DD</p>\n<p>Background color</p>\n<p>This contains the background color of the image to be displayed while the image is loading</p>\n<p>Length 00 00 00 06 = 6 bytes long</p>\n<p>Type 62 4B 47 44 = bKGD</p>\n<p>Data</p>\n<p> 00 FF 00 FF 00 FF\n= white</p>\n<p>CRC A0 BD A7 93</p>\n<p>Image data</p>\n<p>This contains the compressed image data.</p>\n<p>Length 00 00 F5 51 = 62801 bytes long</p>\n<p>Type 49 44 41 54 = IDAT</p>\n<p>Data</p>\n<p>...\n(the compressed and filtered bytes of the image)</p>\n<p>CRC BF EB 1B 15</p>\n<p>Text</p>\n<p>This can store arbitrary tagged textual data about the image.</p>\n<p>Length 00 00 00 27 = 39 bytes long</p>\n<p>Type 74 45 58 74 = tEXt</p>\n<p>Data</p>\n<p> 46 69 6C 65 00 2F 68 6F 6D 65 2F 73 6D 69 74 2F 50 69 63 74 75 72 65 73 2F 69 63 6F 6E 33 2F 33 64 2E 62 6C 65 6E 64\n= key: &quot;File&quot;, value: &quot;/home/smit/Pictures/icon3/3d.blend&quot;</p>\n<p>CRC 88 DA 55 E7</p>\n<p>Text</p>\n<p>This can store arbitrary tagged textual data about the image.</p>\n<p>Length 00 00 00 13 = 19 bytes long</p>\n<p>Type 74 45 58 74 = tEXt</p>\n<p>Data</p>\n<p> 52 65 6E 64 65 72 54 69 6D 65 00 30 30 3A 31 32 2E 31 35\n= key: &quot;RenderTime&quot;, value: &quot;00:12.15&quot;</p>\n<p>CRC 5E 2F 7A B4</p>\n<p>Trailer</p>\n<p>This empty chunk indicates the end of the PNG file.</p>\n<p>Length00 00 00 00 = 0 bytes long</p>\n<p>Type49 45 4E 44 = IEND</p>\n<p>CRCAE 42 60 82</p>\n<h2 id=\"extracting-the-image-data\">Extracting the image data</h2>\n<p>We take all of the <code>IDAT</code> chunks and concatenate them together. In this image, there is only one <code>IDAT</code> chunk. Some images have multiple IDAT chunks, and the contents are concatenated together by the PNG decoder. This is so streaming encoders <a href=\"https://stackoverflow.com/a/29517059/10113238\" rel=\"nofollow ugc noopener\">don&#39;t need to know the total data length up front</a> since the data length is at the beginning of each chunk. Multiple IDAT chunks are also needed for encoding images with image data having a length longer than the largest possible chunk size (2554) Here&#39;s what we get if treat the compressed data as raw image data:</p>\n<figure><img src=\"https://smitop.com/fmat/png/idat-raw.png\" alt=\"Random noise on the top third, with the rest black.\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>It looks like random noise, and that means that the compression algorithm did a good job. Compressed data should look higher-entropy than the lower-entropy data it is encoding. Also, it&#39;s less than a third of the height of the actual image: that&#39;s some good compression!</p>\n<h2 id=\"decompressing\">Decompressing</h2>\n<p>The most important chunk is the <code>IDAT</code> chunk, which contains the actual image data. To get the filtered image data, we concatenate the data in the <code>IDAT</code> chunks, and decompress it using zlib. There is no image-specific compression mechanism in play here: just normal zlib compression.</p>\n<figure><img src=\"https://smitop.com/fmat/png/idat.png\" alt=\"The example image, increasingly skewed to right from top to bottom. It is mostly black and white, with pixels of intense color scattered throughout.\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>Aside from the colors looking all wrong, the image also appears to be skewed horizontally. This is because each line of the image has a filter as the first byte. Filters don&#39;t directly reduce the image size, but get it into a form that is more compressible by zlib. I <a href=\"https://observablehq.com/@smitop/png-filters\" rel=\"nofollow ugc noopener\">have written about PNG filters before</a>.</p>\n<h2 id=\"defiltering\">Defiltering</h2>\n<p>We take the decompressed data and undo the filter on each line. This gets us the decoded image, the same as the original! Here&#39;s the popularity of each filter type:</p>\n<p>None 0</p>\n<p>Subtract 23</p>\n<p>Up 77</p>\n<p>Average 134</p>\n<p>Paeth 22</p>\n<figure><img src=\"https://smitop.com/fmat/png/defiltered.png\" alt=\"The example image\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<h2 id=\"interlacing\">Interlacing</h2>\n<p>PNGs can optionally be interlaced, which splits the image into 7 different images, each of which covers a non-overlapping area of the image:</p>\n<figure><img src=\"https://smitop.com/ext/Adam7-mul.svg\" alt=\"Diagram of the 7 passes in an 8x8 area. The first is a single pixel in the top-left, and the seventh is every other row.\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>Each of the 7 images is loaded in sequence, adding more detail to the image as it is loaded. This sequence of images is called <a href=\"https://en.wikipedia.org/wiki/Adam7_algorithm\" rel=\"nofollow ugc noopener\">Adam7</a>.</p>\n<p>The data for each of the 7 images are stored after each other in the image file. If we take the example image and enable interlacing, here&#39;s the raw uncompressed image data:</p>\n<figure><img src=\"https://smitop.com/fmat/png/inter-idat.png\" alt=\"Same as the earlier skewed letter S, but there are multiple stacked on top of each other. The bottom half has 2, the third above that has 2 more, and above that it increasingly looks like random noise.\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>Here are the 7 passes that we can extract from that image data, which look like downscaled versions of the image:</p>\n<figure><img src=\"https://smitop.com/fmat/png/inter-1.png\" alt=\"\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<figure><img src=\"https://smitop.com/fmat/png/inter-2.png\" alt=\"\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<figure><img src=\"https://smitop.com/fmat/png/inter-3.png\" alt=\"\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<figure><img src=\"https://smitop.com/fmat/png/inter-4.png\" alt=\"\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<figure><img src=\"https://smitop.com/fmat/png/inter-5.png\" alt=\"\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<figure><img src=\"https://smitop.com/fmat/png/inter-6.png\" alt=\"\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<figure><img src=\"https://smitop.com/fmat/png/inter-7.png\" alt=\"\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>Since some of those sub-images are rectangles but the actual image is square, there will be more details horizontally than vertically when loading the image, since horizontal detail is more important than vertical detail for human visual perception.</p>\n<h2 id=\"bonus-bugs\">Bonus: bugs</h2>\n<p>Here&#39;s what you get when you have a bug in the Average filter that causes it to treat overflows incorrectly (the way integer overflow in filter value calculation is specified to be a bit different than the rest):</p>\n<figure><img src=\"https://smitop.com/pngbug.png\" alt=\"The example image, but it looks glitchy starting a quarter of the way down.\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>","headings":[{"level":2,"text":"Chunking","id":"chunking"},{"level":2,"text":"Extracting the image data","id":"extracting-the-image-data"},{"level":2,"text":"Decompressing","id":"decompressing"},{"level":2,"text":"Defiltering","id":"defiltering"},{"level":2,"text":"Interlacing","id":"interlacing"},{"level":2,"text":"Bonus: bugs","id":"bonus-bugs"}]}}