<?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: PlainlyKit</title>
    <description>The latest articles on DEV Community by PlainlyKit (@plainlykit).</description>
    <link>https://dev.to/plainlykit</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%2F4204736%2F50c40d36-5a20-4cb7-9d71-8bf441b2986a.png</url>
      <title>DEV Community: PlainlyKit</title>
      <link>https://dev.to/plainlykit</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/plainlykit"/>
    <language>en</language>
    <item>
      <title>A byte limit is not a pixel limit: guardrails for browser image conversion</title>
      <dc:creator>PlainlyKit</dc:creator>
      <pubDate>Sat, 24 Oct 2026 13:14:33 +0000</pubDate>
      <link>https://dev.to/plainlykit/a-byte-limit-is-not-a-pixel-limit-guardrails-for-browser-image-conversion-4912</link>
      <guid>https://dev.to/plainlykit/a-byte-limit-is-not-a-pixel-limit-guardrails-for-browser-image-conversion-4912</guid>
      <description>&lt;p&gt;A 4 MB image file does not imply a 4 MB working set. Compressed input size and decoded raster size describe different things. That distinction matters when a browser tool accepts AVIF and exports PNG.&lt;/p&gt;

&lt;p&gt;Disclosure: I work on PlainlyKit, which has a browser image converter. This article was prepared with AI assistance and checked against the implementation. The examples below are deliberately small teaching examples, not the complete converter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with a memory estimate you can explain
&lt;/h2&gt;

&lt;p&gt;For an ordinary 8-bit RGBA raster, a useful baseline is:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;width × height × 4 bytes&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;For 5,000 × 5,000 pixels, that is 100,000,000 bytes, or approximately 95.4 MiB, for one such raster. This is arithmetic, not a measurement of a browser's actual allocation.&lt;/p&gt;

&lt;p&gt;The decoder, bitmap, canvas, encoded output and previews can coexist. Browser implementations may use other internal representations. A small compressed input can therefore lead to much more work than its file size suggests.&lt;/p&gt;

&lt;p&gt;Use separate limits for compressed bytes, dimensions, pixel count and retained output. None is a substitute for the others.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reject both excessive area and excessive edge length
&lt;/h2&gt;

&lt;p&gt;Here is a dimension guard with a 25-million-pixel budget and a 16,384-pixel edge limit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;assertRasterSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isSafeInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isSafeInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
      &lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RangeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid image dimensions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="nx"&gt;_384&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="nx"&gt;_384&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
      &lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="nx"&gt;_000_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RangeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Image exceeds this tool's raster budget&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;height&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;These are application limits, not universal browser capabilities. Passing the guard does not guarantee that a particular phone can allocate the required canvas.&lt;/p&gt;

&lt;p&gt;The edge check catches a very long, narrow image that might fit the area budget. The area check catches a large square whose edges individually fit. Validate both before allocating the output canvas.&lt;/p&gt;

&lt;p&gt;There is an important timing limitation: if you obtain dimensions by decoding with &lt;code&gt;createImageBitmap()&lt;/code&gt;, decoding has already happened when this guard runs. It can prevent subsequent canvas allocation and output work; it cannot retroactively protect the decoder. A byte cap and format-header checks are still useful, and none of these checks is a complete defense against hostile image files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat PNG encoding as a fallible operation
&lt;/h2&gt;

&lt;p&gt;Canvas export is asynchronous. MDN documents that &lt;code&gt;toBlob()&lt;/code&gt; can deliver &lt;code&gt;null&lt;/code&gt;, so the download button should not assume that a file exists:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;pngBlob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBlob&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&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="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PNG encoding did not produce a valid output blob&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&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;This checks the returned blob's label and size; it is not a full PNG file validator. PNG is a required canvas output format, but memory and encoding failures still need an error path. Disable the previous download whenever a new conversion starts or inputs change, and only expose the new blob after success.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bound the batch, not just each file
&lt;/h2&gt;

&lt;p&gt;Ten files passing a per-file check can still be too much when decoded together. Process sequentially rather than decoding the whole selection with &lt;code&gt;Promise.all()&lt;/code&gt;. Keep an aggregate input limit and an aggregate retained-output limit as well.&lt;/p&gt;

&lt;p&gt;Sequential conversion reduces concurrent decoding work. It does not make memory constant if you retain every output blob or preview. Release previews you no longer need, and use &lt;code&gt;ImageBitmap.close()&lt;/code&gt; when you are finished with an image bitmap. Cleanup also belongs on error and cancellation paths.&lt;/p&gt;

&lt;h2&gt;
  
  
  State the fidelity boundary
&lt;/h2&gt;

&lt;p&gt;PNG compression does not restore detail already lost in an AVIF source. It stores the pixels handed to its encoder without another lossy compression step. A browser decode-and-canvas pipeline should not be presented as preserving every source metadata field, HDR characteristic or wide-gamut color value.&lt;/p&gt;

&lt;p&gt;Likewise, a still-image example is not an animation converter. Decide how to handle animated input explicitly. Our implementation rejects animated AVIF rather than silently claiming that a single-frame PNG preserves the sequence.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://plainlykit.com/tools/avif-to-png" rel="noopener noreferrer"&gt;PlainlyKit's AVIF-to-PNG tool&lt;/a&gt;, the current limits are 10 still images, 20 MiB per file, 50 MiB total input, 25 million pixels per image, 16,384 pixels per edge and 50 MiB total retained output. The point is to expose practical limits before the user selects a large batch, not to promise a fixed peak-memory figure.&lt;/p&gt;

&lt;p&gt;The accompanying local self-check exercised the dimension boundaries and the encoding wrapper's success, null, empty-blob and wrong-MIME paths using mocks. It did not measure browser memory, test actual AVIF conversion across browsers or establish color fidelity.&lt;/p&gt;

&lt;p&gt;References: &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob" rel="noopener noreferrer"&gt;MDN: canvas.toBlob()&lt;/a&gt;, &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/ImageBitmap/close" rel="noopener noreferrer"&gt;MDN: ImageBitmap.close()&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
