{"id":4229,"date":"2016-02-19T06:40:56","date_gmt":"2016-02-19T12:40:56","guid":{"rendered":"http:\/\/www.realtimerendering.com\/blog\/?p=4229"},"modified":"2016-02-23T16:47:30","modified_gmt":"2016-02-23T22:47:30","slug":"a-png-puzzle","status":"publish","type":"post","link":"https:\/\/www.realtimerendering.com\/blog\/a-png-puzzle\/","title":{"rendered":"A PNG Puzzle"},"content":{"rendered":"<p><a href=\"http:\/\/www.realtimerendering.com\/blog\/png-srgb-cutoutdecal-aa-problematic\/\">Last post<\/a> was too long, covering too much terrain. Here&#8217;s a puzzle instead which whittles it all down.<\/p>\n<p>What values do you store in an sRGB PNG to display a perceptually half-gray color, with an alpha of 0.5?<\/p>\n<p>If you&#8217;re an absolute expert on PNG and perception and alpha, that&#8217;s all the information you need. Just in case, to make sure you don&#8217;t break any rules, here are the key bits:<\/p>\n<ol>\n<li>A perceptually half-gray color on the screen is (187,187,187), not (128,128,128). See the image below to prove this to yourself, which is from <a href=\"http:\/\/filmicgames.com\/archives\/327\">John Hable&#8217;s lovely article<\/a>.<\/li>\n<li>Your PNG is saving values in <a href=\"https:\/\/en.wikipedia.org\/wiki\/SRGB\">sRGB<\/a> space. No extremely-rare\u00a0<a href=\"http:\/\/www.libpng.org\/pub\/png\/book\/chapter10.html#png.ch10.div.2\">gamma = 1.0 PNG<\/a> for you.<\/li>\n<li>Alpha is coverage. The <a href=\"http:\/\/www.libpng.org\/pub\/png\/spec\/1.2\/PNG-Chunks.html\">PNG spec<\/a> notes, \u201cThe gamma value has no effect on alpha samples, which are always a linear fraction of full opacity.\u201d<\/li>\n<li><a href=\"https:\/\/www.w3.org\/TR\/PNG-DataRep.html\">PNG alphas are unassociated<\/a>, they do not\u00a0premultiply the color. To display your sRGB PNG color\u00a0composited against black, you must multiply it by your unassociated alpha value.<\/li>\n<\/ol>\n<p>So, what do you store in your PNG image to get a half-gray color displayed, with an alpha of 0.5? A few hints, then the answer, is after the image below.<\/p>\n<p>Horizontal fully-black and fully-white lines combine to a half-gray, represented by 187. That&#8217;s sRGB in action:<\/p>\n<div id=\"attachment_4230\" style=\"width: 450px\" class=\"wp-caption alignnone\"><img loading=\"lazy\" decoding=\"async\" aria-describedby=\"caption-attachment-4230\" class=\"wp-image-4230 size-full\" src=\"http:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2016\/02\/SmallLines-WithNumbers.png\" alt=\"SmallLines-WithNumbers\" width=\"440\" height=\"300\" srcset=\"https:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2016\/02\/SmallLines-WithNumbers.png 440w, https:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2016\/02\/SmallLines-WithNumbers-300x205.png 300w\" sizes=\"auto, (max-width: 440px) 100vw, 440px\" \/><p id=\"caption-attachment-4230\" class=\"wp-caption-text\"><a href=\"http:\/\/filmicgames.com\/archives\/327\">John Hable&#8217;s image<\/a><\/p><\/div>\n<p>Hint #1: a half-gray color with an alpha of 1.0 (fully opaque) is stored in a PNG by (187,187,187,255).<\/p>\n<p>Hint #2: if a PNG could store a premultiplied color, the answer would be (187,187,187,128).<\/p>\n<p>Hint #3: to turn a premultiplied color into an unassociated color, <a href=\"https:\/\/www.w3.org\/TR\/PNG-Rationale.html#R.Non-premultiplied-alpha\"><em>divide<\/em>\u00a0the color by the (fractional) alpha<\/a>.<\/p>\n<p>And just to have something between you and the answer, here&#8217;s this, from I wish I knew where.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-4231\" src=\"http:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2016\/02\/d424a80f76e16bf552a09fae02fee808_59445.gif\" alt=\"d424a80f76e16bf552a09fae02fee808_59445\" width=\"150\" height=\"130\" \/><\/p>\n<p>The answer is (255,255,255,128), provided by Mike Chock (aka friedlinguini), who commented on my post &#8211; see the comments below. My answer was definitely wrong, so I&#8217;ll explain why this answer works.<\/p>\n<p>The <a href=\"https:\/\/www.w3.org\/TR\/PNG\/#13Alpha-channel-processing\">PNG spec notes<\/a>, &#8220;This computation should be performed with intensity samples (not gamma-encoded samples)&#8221;. So, to display an sRGB-encoded PNG, you must do the following:<\/p>\n<ol>\n<li>Convert the sRGB color to linear space. For (255,255,255,128) this gives (1.0,1.0,1.0).<\/li>\n<li>Now multiply in the alpha, to get a linear premultiplied value. Times (128\/255) -&gt; 0.5 gives (0.5,0.5,0.5).<\/li>\n<li>Convert this value back to sRGB space and display it. This gives (187,187,187) as the color to display.<\/li>\n<\/ol>\n<p>Me, I thought that PNGs with sRGB values and alphas were displayed by simply multiplying the sRGB by the stored alpha. Wrong! At least, by the spec. How could I think such a crazy thing? Because every viewer and every browser I tested showed this to be how such a PNG was displayed.<\/p>\n<p>So, I&#8217;m very happy to find PNG is not broken; it&#8217;s simply that no one implements it correctly. If you do know some software that does display <a href=\"http:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2016\/02\/sampler_with_gamma_srgb_chunks.png\">this image<\/a> properly (<a href=\"http:\/\/www.realtimerendering.com\/alpha_test.html\">your browser does not<\/a>), let me know &#8211; it&#8217;ll be my example of how things should work.<\/p>\n<p>Update: as usual, Jim Blinn predates my realizations by about 18 years. His article &#8220;<a href=\"http:\/\/ieeexplore.ieee.org\/xpl\/login.jsp?tp=&amp;arnumber=637309&amp;url=http%3A%2F%2Fieeexplore.ieee.org%2Fxpls%2Fabs_all.jsp%3Farnumber%3D637309\">A Ghost in a Snowstorm<\/a>&#8221; (collected in the book\u00a0<em><a href=\"http:\/\/smile.amazon.com\/Jim-Blinns-Corner-Notation-Kaufmann\/dp\/1558608605?tag=realtimerenderin\">Notation, Notation, Notation<\/a><\/em>; most of this article can be found <a href=\"https:\/\/books.google.com\/books?id=pPXSXrZcAhsC&amp;pg=PA133&amp;source=gbs_toc_r&amp;cad=4#v=onepage&amp;q&amp;f=false\">here<\/a>) talks about the right way (linearization) and the errors caused by the various wrong ways of encoding alpha and sRGB. Thanks to Sean Barrett for pointing it out.<\/p>\n<p>My conclusion remains the same:\u00a0if you want fun\u00a0puzzles and you&#8217;re near a big city, check out <a href=\"http:\/\/www.puzzledpint.com\/\">The Puzzled Pint<\/a>, a great free social puzzle event each month.<\/p>\n<p><strong>For the record, here&#8217;s my original wrong answer:<\/strong><\/p>\n<p>The answer is (373,373,373,128). To display this RGBA correctly, you multiply by the alpha (and divide by 255, since the value 128 represents 0.5) to get (187,187,187).<\/p>\n<p>And that&#8217;s the fatal flaw of sRGB PNGs\u00a0in a nutshell: you can&#8217;t store 373 in 8 bits in a PNG. 16 bits doesn&#8217;t help: PNGs store their values as fractions in the range [0.0, 1.0].<\/p>\n<p>No linearization or filtering or order of operations or any such thing involved, just a simple question. Unfortunately,\u00a0PNG fails.<\/p>\n<p>Wrong answers include:<\/p>\n<ul>\n<li>(187,187,187,128) &#8211; this would work if PNG had a premultiplied mode. It does not, so this color would be multiplied by 0.5 and\u00a0displayed as (94,94,94). That said, this is a fine way to store the data\u00a0if you have a closed system and no one else will ever use your PNGs.<\/li>\n<li>(187,187,187,255) &#8211; this will display correctly, but doesn&#8217;t keep the alpha around.<\/li>\n<li>(255,255,255,128) &#8211; this\u00a0gives you <a href=\"http:\/\/www.realtimerendering.com\/alpha_test.html\">a display value of (128,128,128) for the color<\/a>, which Hable&#8217;s image shows is not a perceptual half-gray. If you used the PNG gamma chunk and set gamma to 1.0, this would work. Almost no one uses this gamma setting (it causes banding unless you use 16 bits) and it&#8217;s rarely supported by most tools.<\/li>\n<li>(255,255,255,187) &#8211; you break the PNG spec by sRGB correcting the alpha. This will actually display correctly, (187,187,187). If you composite this image over some other image with an alpha, this wrong alpha fails.<\/li>\n<li>(255,255,255,187) again &#8211; you decide to &#8220;remember&#8221; the alpha is sRGB corrected and will uncorrect it before using it as an alpha elsewhere.\u00a0If you want to break the spec, better to go with storing a premultiplied color, the first wrong answer. This fix is confusing.<\/li>\n<li>(255,255,255,128) again &#8211;\u00a0you store the correct alpha, but require that you first convert the stored color from sRGB to linear\u00a0before applying the alpha, then convert the color back to sRGB to display it. This will work, but it <a href=\"http:\/\/www.realtimerendering.com\/blog\/png-srgb-cutoutdecal-aa-problematic\/\">defies radiance and alpha theory<\/a>, it&#8217;s convoluted, expensive, super-confusing, not how anyone implements PNG display, and not how the spec reads, as I understand it. Better to just store a premultiplied color.<\/li>\n<\/ul>\n<p>I wish my conclusion was wrong, but I don&#8217;t see any solution short of adding a new chunk to the PNG spec. My preference is adding a chunk that notes the values are stored as premultiplied.<\/p>\n<p>In the meantime, if you want solvable puzzles and you&#8217;re near a big city, check out <a href=\"http:\/\/www.puzzledpint.com\/\">The Puzzled Pint<\/a>, a great free social puzzle event each month.<\/p>\n<p><em>Addendum<\/em><\/p>\n<p>Zap Andersson debated this puzzle with me on Facebook, and many thanks to him. He prefers the solution (255,255,255,128), applying the alpha &#8220;later.&#8221; To clarify, here&#8217;s how PNGs are normally interpreted (and I think this follows the spec, though I&#8217;d be happy to be proven wrong, as then PNG would still work, even if no viewer or browser I know currently\u00a0implements it correctly):<\/p>\n<p>To display a PNG RGBA in sRGB: you multiply the RGB color by the alpha (expressed as a fraction).<\/p>\n<p>The &#8220;later&#8221; solution to display a PNG RGBA in sRGB: you convert the sRGB number stored to a linear value, you then apply the alpha, and then you convert this linear value back to sRGB for display.<\/p>\n<p>I like this, as convoluted as it is, in that it makes PNG work (I really don&#8217;t want to see PNG fail). The problem with this solution is that I don&#8217;t think anyone does it this way; <a href=\"http:\/\/www.realtimerendering.com\/alpha_test.html\">browsers certainly don&#8217;t<\/a>.<\/p>\n<p>The other interesting thing Zap points out is <a href=\"http:\/\/spitzak.github.io\/conversion\/transform.html\">this interesting page<\/a>, which points to this <a href=\"http:\/\/spitzak.github.io\/conversion\/notlinear.html\">even more relevant page<\/a>. My takeaway is that I shouldn&#8217;t talk about 187-gray as the perceptually average gray; 128 gray really does look perceptually more acceptable (which is often why gamma correction is justified, that human perception is non-linear along with the monitor &#8211; I forgot). This doesn&#8217;t actually change anything above, the &#8220;half-covered pixel&#8221; example should still get a display level of 187.\u00a0This is confirmed by\u00a0alternating full-black and full-white lines averaging out to 187, for example.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Last post was too long, covering too much terrain. Here&#8217;s a puzzle instead which whittles it all down. What values do you store in an sRGB PNG to display a perceptually half-gray color, with an alpha of 0.5? If you&#8217;re an absolute expert on PNG and perception and alpha, that&#8217;s all the information you need. [&hellip;]<\/p>\n","protected":false},"author":3,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[256,600,261,348],"class_list":["post-4229","post","type-post","status-publish","format-standard","hentry","category-misc","tag-gamma","tag-linearization","tag-png","tag-srgb"],"_links":{"self":[{"href":"https:\/\/www.realtimerendering.com\/blog\/wp-json\/wp\/v2\/posts\/4229","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.realtimerendering.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.realtimerendering.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.realtimerendering.com\/blog\/wp-json\/wp\/v2\/users\/3"}],"replies":[{"embeddable":true,"href":"https:\/\/www.realtimerendering.com\/blog\/wp-json\/wp\/v2\/comments?post=4229"}],"version-history":[{"count":21,"href":"https:\/\/www.realtimerendering.com\/blog\/wp-json\/wp\/v2\/posts\/4229\/revisions"}],"predecessor-version":[{"id":4270,"href":"https:\/\/www.realtimerendering.com\/blog\/wp-json\/wp\/v2\/posts\/4229\/revisions\/4270"}],"wp:attachment":[{"href":"https:\/\/www.realtimerendering.com\/blog\/wp-json\/wp\/v2\/media?parent=4229"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.realtimerendering.com\/blog\/wp-json\/wp\/v2\/categories?post=4229"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.realtimerendering.com\/blog\/wp-json\/wp\/v2\/tags?post=4229"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}