<rss version="2.0">
  <channel>
    <title>Png on LLBBL Blog</title>
    <link>https://llbbl.blog/categories/png/</link>
    <description></description>
    
    <language>en</language>
    
    <lastBuildDate>Fri, 14 Aug 2026 10:00:00 -0500</lastBuildDate>
    
    <item>
      <title>How PNG Actually Stores Your Pixels</title>
      <link>https://llbbl.blog/2026/08/14/how-png-actually-stores-your.html</link>
      <pubDate>Fri, 14 Aug 2026 10:00:00 -0500</pubDate>
      
      <guid>http://llbbl.micro.blog/2026/08/14/how-png-actually-stores-your.html</guid>
      <description>&lt;p&gt;I&amp;rsquo;m starting a series on file formats. Not &amp;ldquo;here are the ten image formats you should know,&amp;rdquo; but the actual bytes: what&amp;rsquo;s in the file, in what order, and why someone decided it should be that way.&lt;/p&gt;
&lt;p&gt;Starting with PNG, because it&amp;rsquo;s the format most developers touch every day and almost nobody has looked inside.&lt;/p&gt;
&lt;p&gt;I am likely to cover a few things that other explainer documents have covered, such as chunk structure and chunk types. However, I&amp;rsquo;d like to dig into some details that are not often mentioned, such as where your pixels went.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;a-format-born-from-a-patent-fight&#34;&gt;A Format Born From a Patent Fight&lt;/h2&gt;
&lt;p&gt;PNG exists because of a licensing ambush. On 28 December 1994, right in the middle of the holidays, Unisys announced an agreement to start collecting royalties from authors of GIF-supporting software, on the strength of its patent on the LZW compression algorithm that GIF used.&lt;/p&gt;
&lt;p&gt;The response was fast. A draft for a replacement format was posted to &lt;code&gt;comp.graphics&lt;/code&gt; on 4 January 1995, one week later. It was originally called PBF, for Portable Bitmap Format, and got renamed to PNG two days after that. The format shipped as a W3C Recommendation in October 1996.&lt;/p&gt;
&lt;p&gt;Two things about that origin still show in the bytes. The format is aggressively defensive, because it was designed by people who expected files to be mangled in transit. And it is aggressively extensible, because they had just watched a format become unusable for reasons that had nothing to do with its technical design.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;eight-bytes-of-paranoia&#34;&gt;Eight Bytes of Paranoia&lt;/h2&gt;
&lt;p&gt;Every PNG starts with the same eight bytes:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;Hexadecimal:  89  50  4E  47  0D  0A  1A  0A
ASCII/Ctrl: \x89  P   N   G  \r  \n \x1A \n
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;P N G&lt;/code&gt; in the middle is obvious. The other five bytes are a booby trap for 1995-era file transfer, and each one catches a specific failure:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;0x89&lt;/code&gt; has the high bit set. Some 7-bit transfer paths stripped bit 7 from every byte. If that happened, this byte arrives as &lt;code&gt;0x09&lt;/code&gt; and the file is detectably wrong on byte one.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0x0D 0x0A&lt;/code&gt; is a DOS line ending. A text-mode FTP transfer that &amp;ldquo;helpfully&amp;rdquo; converts CRLF to LF mangles it.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0x1A&lt;/code&gt; is Ctrl-Z, the MS-DOS end-of-file marker. If you &lt;code&gt;TYPE&lt;/code&gt; a PNG at a DOS prompt, output stops here instead of spraying binary at your terminal and leaving it in a weird state.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0x0A&lt;/code&gt; is a bare LF, catching the opposite conversion: LF silently expanded to CRLF.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Who would have thought that so many bits were used just to account for line endings in different operating systems? I suppose it&amp;rsquo;s good to plan ahead when designing a file format.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;everything-is-a-chunk&#34;&gt;Everything Is a Chunk&lt;/h2&gt;
&lt;p&gt;After the signature, a PNG is a flat sequence of chunks. No central directory, no offset table. You read them in order.&lt;/p&gt;
&lt;p&gt;Every chunk has the same four-field shape:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;Field&lt;/th&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;Size&lt;/th&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;Notes&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;Length&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;4 bytes&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;Big-endian, counts &lt;strong&gt;only&lt;/strong&gt; the data field&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;Chunk Type&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;4 bytes&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;Four ASCII letters&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;Chunk Data&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;Length bytes&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;Can be zero-length&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;CRC-32&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;4 bytes&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;Computed over type &lt;strong&gt;and&lt;/strong&gt; data, not over length&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Two details worth keeping. The length field is 32 bits but the spec caps values at 2³¹−1, so the high bit is always clear. And the CRC covers the type plus the data but skips the length, which means a corrupted length field is not detected by the chunk&amp;rsquo;s own checksum.&lt;/p&gt;
&lt;p&gt;The chunk type is where PNG does something clever. Those four letters are ASCII, and bit 5 of an ASCII letter is what distinguishes uppercase from lowercase (&lt;code&gt;A&lt;/code&gt; is &lt;code&gt;0x41&lt;/code&gt;, &lt;code&gt;a&lt;/code&gt; is &lt;code&gt;0x61&lt;/code&gt;). PNG uses that bit in each of the four positions as a flag:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;Position&lt;/th&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;Uppercase means&lt;/th&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;Lowercase means&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;1st&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;Critical: decoder must understand it&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;Ancillary: safe to ignore&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;2nd&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;Public, registered in the spec&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;Private, vendor-specific&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;3rd&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;Reserved, must be uppercase today&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;(reserved for future use)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;4th&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;Unsafe to copy if pixels changed&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;Safe to copy blindly&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;So a decoder that has never heard of &lt;code&gt;tEXt&lt;/code&gt; can tell from the lowercase &lt;code&gt;t&lt;/code&gt; that skipping it is fine. A decoder hitting &lt;code&gt;IDAT&lt;/code&gt; sees the uppercase &lt;code&gt;I&lt;/code&gt; and knows it cannot skip it. The capability negotiation is encoded in the name itself, which means you can add chunk types decades later without breaking old readers. This is why APNG could bolt animation onto PNG without a version bump.&lt;/p&gt;
&lt;p&gt;Four chunk types are critical: &lt;code&gt;IHDR&lt;/code&gt; (header, always first), &lt;code&gt;PLTE&lt;/code&gt; (palette), &lt;code&gt;IDAT&lt;/code&gt; (the pixels), and &lt;code&gt;IEND&lt;/code&gt; (a zero-length terminator).&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;ihdr-is-the-decode-key&#34;&gt;IHDR Is the Decode Key&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;IHDR&lt;/code&gt; is exactly 13 bytes and it comes first because nothing else can be interpreted without it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Width&lt;/strong&gt; (4 bytes) and &lt;strong&gt;Height&lt;/strong&gt; (4 bytes), big-endian&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bit depth&lt;/strong&gt; (1 byte): bits per &lt;em&gt;sample&lt;/em&gt;, one of 1, 2, 4, 8, 16&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Color type&lt;/strong&gt; (1 byte): what a pixel is made of&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compression method&lt;/strong&gt; (1 byte): always 0&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Filter method&lt;/strong&gt; (1 byte): always 0&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Interlace method&lt;/strong&gt; (1 byte): 0 for none, 1 for Adam7&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Bit depth and color type together determine everything about the pixel layout, and only certain combinations are legal:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;Color type&lt;/th&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;Name&lt;/th&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;Samples per pixel&lt;/th&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;Legal bit depths&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;0&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;Greyscale&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;1&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;1, 2, 4, 8, 16&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;2&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;Truecolor (RGB)&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;3&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;8, 16&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;3&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;Indexed&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;1 (a palette index)&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;1, 2, 4, 8&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;4&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;Greyscale + alpha&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;2&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;8, 16&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;6&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;Truecolor + alpha (RGBA)&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;4&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;8, 16&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Note the gaps. You cannot have 16-bit indexed color, because a palette holds at most 256 entries and 8 bits already addresses all of them. You cannot have 1-bit RGB, because a &amp;ldquo;1-bit red sample&amp;rdquo; isn&amp;rsquo;t a useful thing. The table isn&amp;rsquo;t arbitrary; each missing cell is a combination that would be incoherent.&lt;/p&gt;
&lt;p&gt;Also note that bit depth is per &lt;em&gt;sample&lt;/em&gt;, not per pixel. A bit depth of 16 with color type 6 means 16 bits each for R, G, B, and A: 64 bits per pixel. That&amp;rsquo;s the &amp;ldquo;64-bit RGBA&amp;rdquo; you see in PNG marketing.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;where-the-pixels-actually-live&#34;&gt;Where the Pixels Actually Live&lt;/h2&gt;
&lt;p&gt;Uncompress all the &lt;code&gt;IDAT&lt;/code&gt; data and concatenate it, and you get a byte stream. That stream is not a grid. It&amp;rsquo;s a sequence of &lt;strong&gt;scanlines&lt;/strong&gt;, one per image row, top to bottom. And each scanline is:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;[1 filter type byte][packed sample data for the whole row]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;That leading byte is not pixel data. It&amp;rsquo;s a number from 0 to 4 saying which filter was applied to this row.&lt;/p&gt;
&lt;p&gt;The sample data is packed with no padding between pixels and no separators. Samples appear in a fixed order within each pixel:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Greyscale: &lt;code&gt;grey&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Truecolor: &lt;code&gt;red, green, blue&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Indexed: &lt;code&gt;palette index&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Greyscale + alpha: &lt;code&gt;grey, alpha&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Truecolor + alpha: &lt;code&gt;red, green, blue, alpha&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here is an example PNG, filter type 0 (no filtering) on both rows:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;scanline 0: 00 ff 00 00 00 ff 00 00 00 ff ff ff 00
            ^^ filter byte
            ^^^^^^^^ red pixel (ff,00,00)
                     ^^^^^^^^ green pixel (00,ff,00)

scanline 1: 00 00 00 00 80 80 80 ff ff ff ff 00 ff
            ^^ filter byte
               ^^^^^^^^ black    ^^^^^^^^ white
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Twelve bytes of pixel data per row (4 pixels × 3 samples), each prefixed by one filter byte, for 26 bytes of raw stream. The complete file, signature and all four chunks included, is &lt;strong&gt;83 bytes&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The whole model at 8-bit depth: walk the row, emit samples in order, move on. No alignment, no padding, no per-pixel headers.&lt;/p&gt;
&lt;h3 id=&#34;below-8-bits-pixels-share-bytes&#34;&gt;Below 8 Bits, Pixels Share Bytes&lt;/h3&gt;
&lt;p&gt;Bit depths of 1, 2, and 4 only apply to greyscale and indexed images. Multiple pixels get packed into a single byte.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;These samples are packed into bytes with the leftmost sample in the high-order bits of a byte followed by the other samples for the scanline.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Leftmost pixel goes in the &lt;strong&gt;high&lt;/strong&gt; bits. So for a 12-pixel-wide 1-bit greyscale image:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;pixels       : 1 1 0 1 0 0 0 1  1 0 1 1
packed bytes : 0xd1 0xb0
               11010001 10110000
                              ^^^^ unused
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Twelve pixels need 12 bits, which rounds up to 2 bytes, leaving 4 bits spare at the end. The spec&amp;rsquo;s language on those leftover bits:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;When there are multiple pixels per byte, some low-order bits of the last byte of a scanline may go unused. The contents of these unused bits are not specified.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Scanlines always start on a byte boundary. Row 2 never continues in the leftover bits of row 1&amp;rsquo;s last byte.&lt;/p&gt;
&lt;p&gt;At bit depth 16, each sample is two bytes, most significant byte first. The spec calls it network byte order. On x86 and ARM, which are little-endian, that means every 16-bit sample needs a byte swap on read and on write.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;the-filter-byte-is-the-whole-trick&#34;&gt;The Filter Byte Is the Whole Trick&lt;/h2&gt;
&lt;p&gt;Now back to that leading byte on every scanline.&lt;/p&gt;
&lt;p&gt;PNG uses DEFLATE, the same algorithm as gzip and zip. If you just DEFLATE&amp;rsquo;d raw pixels, PNG would compress about as well as gzipping a bitmap, which is to say barely at all. Photographs and gradients don&amp;rsquo;t repeat exact byte sequences, and LZ77 needs exact repeats.&lt;/p&gt;
&lt;p&gt;So before compressing, PNG transforms each scanline into differences from its neighbors. Five filters are available, chosen &lt;strong&gt;per scanline&lt;/strong&gt;:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;Type&lt;/th&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;Name&lt;/th&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;Transform&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;0&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;None&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;store the byte as-is&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;1&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;Sub&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;subtract the byte from the pixel to the left&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;2&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;Up&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;subtract the byte from the pixel above&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;3&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;Average&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;subtract the average of left and above&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;4&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;Paeth&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;subtract whichever of left/above/upper-left is the best predictor&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;All arithmetic is mod 256, which is what makes it reversible without storing a sign. And &amp;ldquo;the pixel to the left&amp;rdquo; means the byte at the same position in the previous pixel, so for RGB the red sample is compared against the previous red sample, not against the previous blue.&lt;/p&gt;
&lt;p&gt;Take a 16-pixel greyscale gradient stepping by 10:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;raw scanline     : 00 0a 14 1e 28 32 3c 46 50 5a 64 6e 78 82 8c 96
after Sub filter : 00 0a 0a 0a 0a 0a 0a 0a 0a 0a 0a 0a 0a 0a 0a 0a
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Sixteen distinct byte values become two. The image is unchanged and the transform is exactly reversible, but LZ77 now sees a run it can encode in almost nothing. On this toy row, DEFLATE produces 28 bytes for the raw version and 15 for the filtered one.&lt;/p&gt;
&lt;p&gt;Sixteen bytes is far too small for DEFLATE to stretch its legs, so don&amp;rsquo;t read that ratio as typical. The point is the entropy collapse: filtering doesn&amp;rsquo;t compress anything, it rearranges the data so the compressor has something to find.&lt;/p&gt;
&lt;p&gt;That per-scanline choice is also why two encoders produce different-sized files from identical pixels. libpng, ImageMagick, &lt;code&gt;oxipng&lt;/code&gt;, and &lt;code&gt;zopflipng&lt;/code&gt; all ship different filter-selection heuristics. Same spec, same decoded output, different bytes on disk. Most PNG optimizers are search algorithms over filter choices, not better compressors.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;the-compression-pipeline-end-to-end&#34;&gt;The Compression Pipeline, End to End&lt;/h2&gt;
&lt;p&gt;Putting it together:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;raw pixels
  -&amp;gt; pack into scanlines (samples in order, sub-byte packing if needed)
  -&amp;gt; prepend a filter byte per scanline, apply the filter
  -&amp;gt; DEFLATE the whole concatenated stream (LZ77 + Huffman)
  -&amp;gt; wrap in a zlib container (RFC 1950)
  -&amp;gt; split across one or more IDAT chunks
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;A few consequences fall out of that ordering:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The zlib stream spans chunks.&lt;/strong&gt; &lt;code&gt;IDAT&lt;/code&gt; boundaries are arbitrary. A decoder must concatenate every &lt;code&gt;IDAT&lt;/code&gt; payload and then decompress; decompressing them individually fails. Encoders split them for streaming, not for structure.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Adler-32 checksum in the zlib wrapper covers filtered bytes&lt;/strong&gt;, not your original pixels. It validates decompression, not image fidelity. The per-chunk CRC-32 is what protects against transmission corruption.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Compression is global across the image.&lt;/strong&gt; LZ77&amp;rsquo;s 32KB sliding window means row 400 can match against row 380 if they&amp;rsquo;re similar. This is why a 64×64 solid color block compresses to &lt;strong&gt;136 bytes&lt;/strong&gt; while a 64×64 gradient of the same dimensions takes &lt;strong&gt;10,362 bytes&lt;/strong&gt;, against 12,288 bytes raw. Uniformity compresses; novelty doesn&amp;rsquo;t.&lt;/p&gt;
&lt;p&gt;And a practical one: for that solid-color block, encoding as &lt;strong&gt;indexed&lt;/strong&gt; color with a one-entry palette produces a &lt;strong&gt;99-byte&lt;/strong&gt; file instead of 136, because each pixel is one index byte instead of three samples. If your image has few colors, color type 3 usually beats truecolor even after DEFLATE gets its turn.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;interlacing-briefly&#34;&gt;Interlacing, Briefly&lt;/h2&gt;
&lt;p&gt;If the interlace byte in &lt;code&gt;IHDR&lt;/code&gt; is 1, the image uses Adam7: the pixels are transmitted in seven passes over an 8×8 grid, coarse to fine, so a partially-downloaded image renders as a low-resolution preview that sharpens.&lt;/p&gt;
&lt;p&gt;Two things to know. Each pass is filtered and encoded as an independent sub-image with its own scanlines and filter bytes, so a decoder can&amp;rsquo;t treat the stream as one grid. And Adam7 typically makes files &lt;em&gt;larger&lt;/em&gt;, because breaking the image into seven sparse sub-images destroys exactly the local coherence that filtering and LZ77 depend on. It was a good trade on a 28.8k modem. On any modern connection it costs size and complexity for a progressive render nobody waits around to see.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;what-this-buys-you&#34;&gt;What This Buys You&lt;/h2&gt;
&lt;p&gt;The design decisions hold up well for a 1996 format:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Unknown chunks are safe by construction&lt;/strong&gt;, so the format extended to EXIF metadata, ICC profiles, and animation without ever breaking old decoders.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Every chunk is individually checksummed&lt;/strong&gt;, so corruption is localized and detectable rather than silently rendering garbage.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Filtering is a preprocessing step, not a compression format&lt;/strong&gt;, which means encoders can get better forever without touching the spec. A file written by &lt;code&gt;zopflipng&lt;/code&gt; today decodes fine in a 1997 reader.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That extensibility is not just historical. PNG got a Third Edition as a W3C Recommendation on 24 June 2025, which finally standardized APNG, added an &lt;code&gt;eXIf&lt;/code&gt; chunk for camera metadata, and brought in HDR through three new chunks (&lt;code&gt;cICP&lt;/code&gt;, &lt;code&gt;mDCV&lt;/code&gt;, &lt;code&gt;cLLI&lt;/code&gt;). Thirty years on, the container still had room.&lt;/p&gt;
&lt;p&gt;Where it shows its age is DEFLATE, which is a 1990s compressor. Lossless WebP does beat it: Google&amp;rsquo;s own study puts WebP lossless at 23% smaller than PNGs already optimized with ZopfliPNG, and 42% smaller than default libpng output. Worth noting the baseline matters enormously there, and Google&amp;rsquo;s WebP FAQ quotes a different figure (26%) than the study it links to.&lt;/p&gt;
&lt;p&gt;Lossless AVIF is a murkier story than the marketing suggests. AOMedia publishes no general lossless-AVIF-versus-PNG number at all; its quantified claims (50% versus JPEG, 30% versus WebP) are all about lossy encoding. The only primary figure available is 10% versus a 16-bit PNG for a single demo image using a new v1.2.0 feature. Independent testing regularly finds lossless AVIF producing &lt;em&gt;larger&lt;/em&gt; files than PNG for flat synthetic images like icons, UI, and charts. If you&amp;rsquo;re picking a format for screenshots and diagrams, test on your own images rather than trusting a general ranking.&lt;/p&gt;
&lt;p&gt;Next in the series: the opposite of all this. A text file, which announces nothing about itself at all.&lt;/p&gt;
&lt;h2 id=&#34;sources&#34;&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://www.w3.org/TR/png-3/&#34;&gt;W3C PNG Specification, Third Edition&lt;/a&gt; — the current standard; §7.2 covers scanlines and sample packing, §9 covers filtering&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://datatracker.ietf.org/doc/html/rfc2083&#34;&gt;RFC 2083&lt;/a&gt; — the original 1997 IETF PNG specification&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://datatracker.ietf.org/doc/html/rfc1950&#34;&gt;RFC 1950 (zlib)&lt;/a&gt; and &lt;a href=&#34;https://datatracker.ietf.org/doc/html/rfc1951&#34;&gt;RFC 1951 (DEFLATE)&lt;/a&gt; — the compression layer&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;http://www.libpng.org/pub/png/pnghist.html&#34;&gt;libpng PNG history&lt;/a&gt; — the Unisys announcement, the PBF name, and the January 1995 timeline&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://developers.google.com/speed/webp/docs/webp_lossless_alpha_study&#34;&gt;WebP Lossless and Alpha Study&lt;/a&gt; — Google&amp;rsquo;s 23%/42% lossless figures and their baselines&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;http://aomedia.org/blog%20posts/AV1-Image-File-Format-Specification-Gets-an-Upgrade-with-AVIF/&#34;&gt;AOMedia on AVIF v1.2.0&lt;/a&gt; — the 10% lossless figure, and its narrow scope&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;I&amp;rsquo;d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at &lt;a href=&#34;https://micro.blog/llbbl?remote_follow=1&#34;&gt;@logan@llbbl.blog&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
</description>
    </item>
    
  </channel>
</rss>