<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: 김찬우</title>
    <description>The latest articles on DEV Community by 김찬우 (@_37957324f11fff423be23).</description>
    <link>https://dev.to/_37957324f11fff423be23</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4060061%2F2dfc00f6-72d6-4c79-9876-b26ed0f90d2d.png</url>
      <title>DEV Community: 김찬우</title>
      <link>https://dev.to/_37957324f11fff423be23</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/_37957324f11fff423be23"/>
    <language>en</language>
    <item>
      <title>I "fixed" the same image bug six times. The cause wasn't dark mode.</title>
      <dc:creator>김찬우</dc:creator>
      <pubDate>Mon, 03 Aug 2026 07:48:57 +0000</pubDate>
      <link>https://dev.to/_37957324f11fff423be23/i-fixed-the-same-image-bug-six-times-the-cause-wasnt-dark-mode-232j</link>
      <guid>https://dev.to/_37957324f11fff423be23/i-fixed-the-same-image-bug-six-times-the-cause-wasnt-dark-mode-232j</guid>
      <description>&lt;p&gt;I'm a first-year undergrad in Jeju, South Korea. I build a study app alone — I'm not a professional developer.&lt;/p&gt;

&lt;p&gt;While preparing to launch it in Japan, I hit a bug where my mascot image rendered almost pure black on the page, but looked completely fine when opened directly in a browser tab. I assumed it was Chrome's forced dark mode and "fixed" it six times based on that assumption.&lt;/p&gt;

&lt;p&gt;When I finally measured it with Playwright, dark mode had nothing to do with it. Not a single pixel changed with the flag on or off.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who this is for&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Anyone using transparent PNGs on a dark-themed UI&lt;/li&gt;
&lt;li&gt;Anyone currently suspecting Chrome's forced dark mode&lt;/li&gt;
&lt;li&gt;Anyone pair-programming with an AI coding agent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What you'll get&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The non-obvious reason images crush to black&lt;/li&gt;
&lt;li&gt;How to kill a hypothesis with measurement instead of guesswork&lt;/li&gt;
&lt;li&gt;A rule for when to stop guessing&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What happened
&lt;/h2&gt;

&lt;p&gt;I placed a cream-coloured rabbit mascot (transparent PNG) on a dark-themed landing page.&lt;/p&gt;

&lt;p&gt;On the page, the rabbit's body merged into the background. Only the outline and the pink of the ears stayed visible. Opened directly: fine. Embedded in the page: crushed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Six wrong fixes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Blaming Chrome's forced dark mode
&lt;/h3&gt;

&lt;p&gt;Chrome's "show web content in dark mode" feature classifies &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; elements by rendered size. Small icons are left alone; large images are treated as photos and have their bright regions darkened.&lt;/p&gt;

&lt;p&gt;My header logo (32px) was fine. The large mascot (160px) was black. The hypothesis fit perfectly.&lt;/p&gt;

&lt;p&gt;Added &lt;code&gt;&amp;lt;meta name="color-scheme" content="dark"&amp;gt;&lt;/code&gt;. No change.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Switching to background-image
&lt;/h3&gt;

&lt;p&gt;The same feature processes CSS background images through a different path, so they're not inverted. Swapped &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; for &lt;code&gt;background-image&lt;/code&gt;. No change.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Wrapping in SVG
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;svg&amp;gt;&amp;lt;image href="..."/&amp;gt;&amp;lt;/svg&amp;gt;&lt;/code&gt; so it isn't recognised as an &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt;. No change.&lt;/p&gt;

&lt;p&gt;Also, my instruction to the AI agent was vague enough that it interpreted "wrap the image in SVG" as "redraw the image in SVG using the original as reference." I got an entirely different animal. Be specific with agents.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Blaming the file
&lt;/h3&gt;

&lt;p&gt;Opened it directly and the colour really was washed out. The background removal step had altered the RGB values.&lt;/p&gt;

&lt;p&gt;Rebuilt from the original, manipulating only the alpha channel with Pillow. Verified RGB matched pixel-for-pixel before swapping it in. Still no change.&lt;/p&gt;

&lt;p&gt;(There was a hole in that verification. More on that below.)&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Blaming colour space and parent CSS
&lt;/h3&gt;

&lt;p&gt;Walked every ancestor up to &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt; checking &lt;code&gt;filter&lt;/code&gt;, &lt;code&gt;mix-blend-mode&lt;/code&gt;, &lt;code&gt;backdrop-filter&lt;/code&gt;, and &lt;code&gt;opacity&lt;/code&gt;. All &lt;code&gt;none&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Finally measuring
&lt;/h3&gt;

&lt;p&gt;At this point I stopped guessing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Screenshot the full page with forced dark mode ON and OFF, then diff
&lt;/span&gt;&lt;span class="n"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;playwright&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--force-dark-mode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--enable-features=WebContentsForceDark&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;max pixel diff across whole page: 0
mean pixel diff: 0.0
differing pixels: 0 / 1,152,000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Toggling forced dark mode changed nothing. Not one pixel.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As a sanity check, I ran the same flags against a bare test page with no &lt;code&gt;color-scheme&lt;/code&gt; declaration. A cream box went from &lt;code&gt;(245,240,230)&lt;/code&gt; to &lt;code&gt;(41,37,30)&lt;/code&gt; — properly inverted. So the flag was working; it just wasn't touching my page.&lt;/p&gt;

&lt;p&gt;I had been chasing a suspect who was never at the scene.&lt;/p&gt;




&lt;h2&gt;
  
  
  What was actually happening
&lt;/h2&gt;

&lt;p&gt;The source image was 1408×768. I was rendering it into a 32px slot in the header and a 128px slot in the hero. That's an 11× to 44× downscale.&lt;/p&gt;

&lt;p&gt;The transparent region was also large, so with &lt;code&gt;object-contain&lt;/code&gt; the rabbit itself rendered even smaller than 32px.&lt;/p&gt;

&lt;p&gt;Measured results:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source coordinate and RGB&lt;/th&gt;
&lt;th&gt;Rendered after downscale&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;(254,165,135)&lt;/code&gt; solid region&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;(231,151,123)&lt;/code&gt; → ~9% darker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;(245,220,200)&lt;/code&gt; body&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;(214,190,170)&lt;/code&gt; → ~13% darker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;(244,218,191)&lt;/code&gt; near alpha edge&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;(22,21,27)&lt;/code&gt; → &lt;strong&gt;near black&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Only the edges collapsed. The image wasn't inverted — something dark bled in during the downscale.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where I'm still guessing
&lt;/h3&gt;

&lt;p&gt;Two candidates for where the dark came from:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Theory 1: the browser's resampling mixed in the background.&lt;/strong&gt; Semi-transparent edge pixels blend with the dark background behind them during downscale. But browsers normally resample with premultiplied alpha, so I'm not convinced this alone explains a collapse this severe.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Theory 2: the transparent pixels were already black.&lt;/strong&gt; Images that have been through a background-removal tool often contain large regions of "fully transparent, but RGB is black." Invisible on their own — but downscale and that black bleeds into the edges.&lt;/p&gt;

&lt;p&gt;In step 4 I verified my rebuilt image matched the original pixel-for-pixel. But since the thing I compared against &lt;em&gt;was&lt;/em&gt; the original, if the original already had black under the transparent areas, the problem survived the check untouched. I thought I had verified something. I hadn't.&lt;/p&gt;

&lt;p&gt;I think theory 2 is more likely. Checking it is trivial — just look at the RGB of pixels where alpha is 0:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;PIL&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Image&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;

&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mascot.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RGBA&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;transparent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[...,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transparent&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;# near black → theory 2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it is theory 2, the fix is to fill the transparent region's RGB with the colour of neighbouring opaque pixels — usually called colour bleed or edge padding. Most game texture tools have it built in.&lt;/p&gt;

&lt;p&gt;I'll update this post when I've checked. If you've hit this before, I'd like to hear which one it was.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I did instead
&lt;/h2&gt;

&lt;p&gt;The proper fix is to prepare an image at the size you'll actually render. Resample with premultiplied alpha, then unpremultiply. Skip that and your edges go dark.&lt;/p&gt;

&lt;p&gt;I had a launch deadline, so I put a white circle behind the mascot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rounded-full bg-white p-2"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Logomark&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whatever the cause, this removes any opportunity for dark colour to bleed in. It also improved the mascot's visibility on the dark theme, so it worked out design-wise too.&lt;/p&gt;

&lt;p&gt;But this removed the symptom. It did not prove the cause.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I took away
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The most plausible hypothesis is the most dangerous one
&lt;/h3&gt;

&lt;p&gt;My first hypothesis explained the symptoms perfectly. Small logo fine, large image black — exactly matching forced dark mode's size-based classification. That's precisely why I didn't question it until attempt five.&lt;/p&gt;

&lt;p&gt;The real mechanism was "larger render slot means a larger downscale ratio, so edge bleeding shows more strongly." Completely different cause, identical symptom.&lt;/p&gt;

&lt;h3&gt;
  
  
  Interrogate what "I verified it" actually means
&lt;/h3&gt;

&lt;p&gt;In step 4 I wrote that I had verified RGB matched pixel-for-pixel. The numbers were correct. I was comparing against the wrong thing, so I learned nothing.&lt;/p&gt;

&lt;p&gt;Verification code passing feels like safety. But what that verification can and cannot rule out lives outside the code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Measuring is cheaper than you think
&lt;/h3&gt;

&lt;p&gt;The Playwright measurement in step 6 took under 30 minutes. Doing it first would have saved five rounds of fixes.&lt;/p&gt;

&lt;p&gt;Guess-and-fix is fine while it's working. But &lt;strong&gt;miss twice and measure.&lt;/strong&gt; That's the right threshold.&lt;/p&gt;

&lt;h3&gt;
  
  
  The same rule applies with AI agents
&lt;/h3&gt;

&lt;p&gt;I delegated most of this work to an AI coding agent. Agents don't question your hypothesis. They implement it, faithfully and fast. So a wrong hypothesis compounds into wrong fixes at high speed.&lt;/p&gt;

&lt;p&gt;At one point I had three layers of stacked workarounds that were becoming a bug source of their own.&lt;/p&gt;

&lt;p&gt;Asking the agent to "measure the cause and report back" rather than "fix this" is faster in the end. That's what finally solved it, in one pass.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I was building this in the first place
&lt;/h2&gt;

&lt;p&gt;Before every exam, past papers appear from somewhere. From club seniors, from lab mates, from a group chat you're in.&lt;/p&gt;

&lt;p&gt;I wasn't in any of them. Same lectures, same hours studying, different starting line.&lt;/p&gt;

&lt;p&gt;This isn't only a Korean thing. In Japan I kept running into the same story — if you're not in a circle, past papers don't reach you. Different countries, identical structure.&lt;/p&gt;

&lt;p&gt;So I built the thing that doesn't need a social network to get: you upload your own course materials, it analyses them per-instructor, and produces likely exam questions plus a condensed summary. Your files stay yours — nothing is shared with other users, and there is no document library.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://carrotly.app" rel="noopener noreferrer"&gt;carrotly.app&lt;/a&gt;&lt;/strong&gt; — free, no signup needed.&lt;/p&gt;

&lt;p&gt;I genuinely have no idea how well it holds up on a European or American syllabus. &lt;strong&gt;If it fails on yours, that's the feedback I want most.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The rabbit in my logo is the mascot that survived six rounds of fixes.&lt;/p&gt;




&lt;p&gt;Thanks for reading. If you've hit the same symptom, I hope this saved you some hours.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>debugging</category>
      <category>showdev</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
