<?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: Krishna Kumar</title>
    <description>The latest articles on DEV Community by Krishna Kumar (@krishna_kumar_ab6745b0861).</description>
    <link>https://dev.to/krishna_kumar_ab6745b0861</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%2F1601663%2Fa47a820b-96b4-4c86-a5e4-917619e19cbe.png</url>
      <title>DEV Community: Krishna Kumar</title>
      <link>https://dev.to/krishna_kumar_ab6745b0861</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/krishna_kumar_ab6745b0861"/>
    <language>en</language>
    <item>
      <title>Tired of uploading 40MB PNGs to your server and then slicing them (React + Canvas)</title>
      <dc:creator>Krishna Kumar</dc:creator>
      <pubDate>Sat, 31 Oct 2026 04:38:24 +0000</pubDate>
      <link>https://dev.to/krishna_kumar_ab6745b0861/tired-of-uploading-40mb-pngs-to-your-server-and-then-slicing-them-react-canvas-2f3c</link>
      <guid>https://dev.to/krishna_kumar_ab6745b0861/tired-of-uploading-40mb-pngs-to-your-server-and-then-slicing-them-react-canvas-2f3c</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fha4nnfunsflhwlgvbrly.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fha4nnfunsflhwlgvbrly.png" alt=" " width="800" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As a  digital comic artists, we all know , we love exporting massive files.&lt;/p&gt;

&lt;p&gt;I run a small indie comic platform called &lt;a href="https://www.zealplane.com" rel="noopener noreferrer"&gt;ZealPlane&lt;/a&gt; and we were putting together chapters for our original series KARMA, G.O.D (unpublished) We’d often send over single vertical strips that were 1600px wide and 30,000px tall from Clip Studio Paint. even upto 45MB, just for a sinlge image.&lt;/p&gt;

&lt;p&gt;You obviously, can't serve a 30,000px tall PNG directly to a mobile browser unless you want your reader's phone to catch fire. Webtoons and Tapas make you chop up long vertical strips into 800x1280 pieces so they lazy-load cleanly as the user scrolls down.&lt;/p&gt;

&lt;p&gt;At first, Like many backend devs, I upload the file to a Node server, pipe it through sharp, zip up the slices, and send it back.&lt;/p&gt;

&lt;p&gt;turned out to be not a great idea.&lt;/p&gt;

&lt;p&gt;When you decode an uncompressed 1600x30000 RGBA image into memory on the server, it eats almost 200MB of raw RAM per request. It took was three or four people slicing chapters at the same time to spike our VPS memory and stall Nginx. Additionally, waiting 40 seconds just to upload a file on a slow connection and download the exact same pixels back felt super dumb.&lt;/p&gt;

&lt;p&gt;So a few weeks ago, I ripped out the backend endpoint and rewrote our Webtoon Slicer to run 100% in the browser. Zero uploads, zero server costs, and it finishes in like 300ms.&lt;/p&gt;

&lt;p&gt;Here are a few weird browser quirks I ran into while building it, in case anyone else is messing with huge images in React.&lt;/p&gt;

&lt;p&gt;Don't use FileReader.readAsDataURL()&lt;/p&gt;

&lt;p&gt;My first instinct was to read the uploaded file as a base64 string, stick it into an &lt;a href="" class="article-body-image-wrapper"&gt;&lt;img&gt;&lt;/a&gt; tag, and draw it onto a .&lt;/p&gt;

&lt;p&gt;Don't do this with 40MB files. Base64 inflates the string size by ~33%, and decoding a massive image on the main thread completely freezes the React UI for two seconds.&lt;/p&gt;

&lt;p&gt;Use createImageBitmap(file) instead. It takes the raw File or Blob directly from the file input and decodes it off the main thread.&lt;/p&gt;

&lt;p&gt;export async function sliceStrip(file, targetWidth = 800, sliceHeight = 1280) {&lt;br&gt;
  // Decodes off-thread without base64 bloat&lt;br&gt;
  const bitmap = await createImageBitmap(file);&lt;/p&gt;

&lt;p&gt;const scale = targetWidth / bitmap.width;&lt;br&gt;
  const totalScaledHeight = Math.round(bitmap.height  scale);&lt;br&gt;
  const count = Math.ceil(totalScaledHeight / sliceHeight);&lt;/p&gt;

&lt;p&gt;const results = [];&lt;/p&gt;

&lt;p&gt;for (let i = 0; i &amp;lt; count; i++) {&lt;br&gt;
    const currentY = i  sliceHeight;&lt;br&gt;
    const h = Math.min(sliceHeight, totalScaledHeight - currentY);&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Map back to the original unscaled image coordinates
const srcY = currentY / scale;
const srcH = h / scale;

const canvas = document.createElement('canvas');
canvas.width = targetWidth;
canvas.height = h;

const ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';

ctx.drawImage(
  bitmap,
  0, srcY, bitmap.width, srcH,
  0, 0, targetWidth, h
);

const blob = await new Promise((res) =&amp;gt; canvas.toBlob(res, 'image/png'));

results.push({
  name: `slice_${String(i + 1).padStart(3, '0')}.png`,
  blob,
  url: URL.createObjectURL(blob),
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;// Crucial: free the GPU texture immediately!&lt;br&gt;
  bitmap.close();&lt;br&gt;
  return results;&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;The 1px white line bug drove me crazy&lt;/p&gt;

&lt;p&gt;Once we got this working, our artists noticed something super annoying. On dark fantasy panels—especially stuff with heavy black/purple gradients like you see in Solo Leveling: Ragnarok's vertical fight scenes—there was occasionally a tiny 1-pixel horizontal grey seam where two sliced images met on mobile screens.&lt;/p&gt;

&lt;p&gt;Turns out, if an artist uploads an odd-width image (say 1240px wide) and you scale it down to 800px, srcY and srcH end up as floating-point decimals like 1984.3333px. When the HTML5 canvas samples sub-pixels along the very bottom edge of a crop, it blends the edge with empty transparent pixels, creating a faint semi-transparent border.&lt;/p&gt;

&lt;p&gt;Forcing integer math on the source crop coordinates (or keeping the artist's source canvas at an exact multiple like 1600px -&amp;gt; 800px) killed the seam lines immediately.&lt;/p&gt;

&lt;p&gt;Clean up your Object URLs or Safari will crash&lt;/p&gt;

&lt;p&gt;One thing you forget when working in a React SPA: URL.createObjectURL(blob) stays pinned in browser RAM until the document unloads or you manually revoke it.&lt;/p&gt;

&lt;p&gt;If an artist slices a 25-panel chapter, realizes they made a typo in a speech bubble, fixes it, and drops the file in again 4 times in a row, your tab is suddenly holding onto 100 uncompressed PNG blobs in memory. Mobile Safari will just hard-reload the tab without warning.&lt;/p&gt;

&lt;p&gt;Whenever the user resets the tool or drops a new file, loop through the old state first:&lt;/p&gt;

&lt;p&gt;useEffect(() =&amp;gt; {&lt;br&gt;
  return () =&amp;gt; {&lt;br&gt;
    slices.forEach((s) =&amp;gt; URL.revokeObjectURL(s.url));&lt;br&gt;
  };&lt;br&gt;
}, [slices]);&lt;/p&gt;

&lt;p&gt;And don't forget bitmap.close() at the end of your slicing function. Garbage collection on ImageBitmap is notoriously lazy in Chromium.&lt;/p&gt;

&lt;p&gt;Anyway, if you draw webcomics or just want to poke around with how it works in the browser, the tool is live here (no login or paywall or anything, it literally just runs in your tab): zealplane.com/tools/webtoon-slicer.&lt;/p&gt;

&lt;p&gt;How are you guys handling large client-side image jobs? Has anyone played around with OffscreenCanvas inside a Web Worker for this yet, or is standard createImageBitmap fast enough for your use cases?&lt;/p&gt;

&lt;p&gt;If you've ever worked with digital comic artists, you know they love exporting massive files. &lt;/p&gt;

&lt;p&gt;I run a small indie comic platform called &lt;a href="https://www.zealplane.com" rel="noopener noreferrer"&gt;ZealPlane&lt;/a&gt;, and when we were putting together chapters for our original series &lt;a href="https://www.zealplane.com/details/6a60d6370cb3db69eafea317-karma" rel="noopener noreferrer"&gt;&lt;em&gt;KARMA&lt;/em&gt;&lt;/a&gt;, our artists would regularly hand over single vertical strips from Clip Studio Paint that were &lt;code&gt;1600px&lt;/code&gt; wide and &lt;code&gt;30,000px&lt;/code&gt; tall. Sometimes a single image was 45MB.&lt;/p&gt;

&lt;p&gt;You obviously can't serve a 30,000px tall PNG directly to a mobile browser unless you want your reader's phone to catch fire. Platforms like Webtoon and Tapas force you to chop long vertical strips into &lt;code&gt;800x1280&lt;/code&gt; chunks so they can lazy-load cleanly as the user scrolls down.&lt;/p&gt;

&lt;p&gt;At first, I did what most backend devs do: upload the file to a Node server, pipe it through &lt;code&gt;sharp&lt;/code&gt;, zip up the slices, and send it back. &lt;/p&gt;

&lt;p&gt;Bad idea. &lt;/p&gt;

&lt;p&gt;When you decode an uncompressed &lt;code&gt;1600x30000&lt;/code&gt; RGBA image into memory on the server, it eats almost 200MB of raw RAM &lt;em&gt;per request&lt;/em&gt;. All it took was three or four people slicing chapters at the same time to spike our VPS memory and stall Nginx. Plus, waiting 40 seconds just to upload a file on a slow connection and download the exact same pixels back felt super dumb.&lt;/p&gt;

&lt;p&gt;So a few weeks ago, I ripped out the backend endpoint and rewrote our &lt;a href="https://zealplane.com/tools/webtoon-slicer" rel="noopener noreferrer"&gt;zealplane.com/tools/webtoon-slicer&lt;/a&gt; to run 100% in the browser. Zero uploads, zero server costs, and it finishes in like 300ms.&lt;/p&gt;

&lt;p&gt;Here are a few weird browser quirks I ran into while building it, in case anyone else is messing with huge images in React.&lt;/p&gt;
&lt;h3&gt;
  
  
  Don't use FileReader.readAsDataURL()
&lt;/h3&gt;

&lt;p&gt;My first instinct was to read the uploaded file as a base64 string, stick it into an &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tag, and draw it onto a &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Don't do this with 40MB files. Base64 inflates the string size by ~33%, and decoding a massive image on the main thread completely freezes the React UI for two seconds.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;createImageBitmap(file)&lt;/code&gt; instead. It takes the raw &lt;code&gt;File&lt;/code&gt; or &lt;code&gt;Blob&lt;/code&gt; directly from the file input and decodes it off the main thread.&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="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sliceStrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;targetWidth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sliceHeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1280&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Decodes off-thread without base64 bloat&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bitmap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createImageBitmap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;targetWidth&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;bitmap&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;totalScaledHeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bitmap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;totalScaledHeight&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;sliceHeight&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&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="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;currentY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;sliceHeight&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sliceHeight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;totalScaledHeight&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;currentY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Map back to the original unscaled image coordinates&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;srcY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;currentY&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;srcH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;canvas&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;targetWidth&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="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt; &lt;span class="o"&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;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2d&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;imageSmoothingEnabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;imageSmoothingQuality&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;bitmap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;srcY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bitmap&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;srcH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;targetWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&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;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`slice_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;padStart&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;.png`&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="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createObjectURL&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="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Crucial: free the GPU texture immediately!&lt;/span&gt;
  &lt;span class="nx"&gt;bitmap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&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;results&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;h3&gt;
  
  
  The 1px white line bug drove me crazy
&lt;/h3&gt;

&lt;p&gt;Once we got this working, our artists noticed something super annoying. On dark fantasy panels—especially stuff with heavy black/purple gradients like you see in &lt;a href="https://www.zealplane.com/news/sung-suho-powers-explained-solo-leveling-ragnarok-shadow-destruction-monarch" rel="noopener noreferrer"&gt;Solo Leveling: Ragnarok's vertical fight scenes&lt;/a&gt;—there was occasionally a tiny 1-pixel horizontal grey seam where two sliced images met on mobile screens.&lt;/p&gt;

&lt;p&gt;Turns out, if an artist uploads an odd-width image (say &lt;code&gt;1240px&lt;/code&gt; wide) and you scale it down to &lt;code&gt;800px&lt;/code&gt;, &lt;code&gt;srcY&lt;/code&gt; and &lt;code&gt;srcH&lt;/code&gt; end up as floating-point decimals like &lt;code&gt;1984.3333px&lt;/code&gt;. When the HTML5 canvas samples sub-pixels along the very bottom edge of a crop, it blends the edge with empty transparent pixels, creating a faint semi-transparent border.&lt;/p&gt;

&lt;p&gt;Forcing integer math on the source crop coordinates (or keeping the artist's source canvas at an exact multiple like &lt;code&gt;1600px -&amp;gt; 800px&lt;/code&gt;) killed the seam lines immediately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Clean up your Object URLs or Safari will crash
&lt;/h3&gt;

&lt;p&gt;One thing you forget when working in a React SPA: &lt;code&gt;URL.createObjectURL(blob)&lt;/code&gt; stays pinned in browser RAM until the document unloads or you manually revoke it. &lt;/p&gt;

&lt;p&gt;If an artist slices a 25-panel chapter, realizes they made a typo in a speech bubble, fixes it, and drops the file in again 4 times in a row, your tab is suddenly holding onto 100 uncompressed PNG blobs in memory. Mobile Safari will just hard-reload the tab without warning.&lt;/p&gt;

&lt;p&gt;Whenever the user resets the tool or drops a new file, loop through the old state first:&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="nf"&gt;useEffect&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;return &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;slices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;revokeObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&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;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;slices&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And don't forget &lt;code&gt;bitmap.close()&lt;/code&gt; at the end of your slicing function. Garbage collection on &lt;code&gt;ImageBitmap&lt;/code&gt; is notoriously lazy in Chromium.&lt;/p&gt;




&lt;p&gt;Anyway, if you draw webcomics or just want to poke around with how it works in the browser, the tool is live here (no login or paywall or anything, it literally just runs in your tab): &lt;a href="https://www.zealplane.com/tools/webtoon-slicer" rel="noopener noreferrer"&gt;zealplane.com/tools/webtoon-slicer&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;How are you guys handling large client-side image jobs? Has anyone played around with &lt;code&gt;OffscreenCanvas&lt;/code&gt; inside a Web Worker for this yet, or is standard &lt;code&gt;createImageBitmap&lt;/code&gt; fast enough for your use cases?&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>react</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
