<?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: Mohamed Fouad</title>
    <description>The latest articles on DEV Community by Mohamed Fouad (@devmfouad).</description>
    <link>https://dev.to/devmfouad</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%2F4159125%2F13809852-c6de-40b2-ba6c-355a065c1208.png</url>
      <title>DEV Community: Mohamed Fouad</title>
      <link>https://dev.to/devmfouad</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/devmfouad"/>
    <language>en</language>
    <item>
      <title>How I Built Browser-Based Image Tools Without Uploading User Files</title>
      <dc:creator>Mohamed Fouad</dc:creator>
      <pubDate>Sat, 03 Oct 2026 06:43:42 +0000</pubDate>
      <link>https://dev.to/devmfouad/how-i-built-browser-based-image-tools-without-uploading-user-files-4391</link>
      <guid>https://dev.to/devmfouad/how-i-built-browser-based-image-tools-without-uploading-user-files-4391</guid>
      <description>&lt;p&gt;I've used plenty of online image tools over the years.&lt;/p&gt;

&lt;p&gt;Resize an image.&lt;/p&gt;

&lt;p&gt;Compress it.&lt;/p&gt;

&lt;p&gt;Convert WebP to JPG.&lt;/p&gt;

&lt;p&gt;Convert HEIC to JPG.&lt;/p&gt;

&lt;p&gt;Turn a few images into a PDF.&lt;/p&gt;

&lt;p&gt;The workflow is usually simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Select file → upload it → wait → download the result.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;But there was always one thing that bothered me:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Why does a simple image operation require sending the user's file to a server?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For many image-processing tasks, the browser already has enough capabilities to do the work locally.&lt;/p&gt;

&lt;p&gt;So I decided to build a small experiment around that idea.&lt;/p&gt;

&lt;p&gt;That experiment became &lt;strong&gt;Pixyro&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pixyro.com?utm_source=dev.to"&gt;Pixyro&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The basic idea
&lt;/h2&gt;

&lt;p&gt;The architecture is intentionally simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User selects an image
        ↓
Browser reads the file
        ↓
Image is decoded locally
        ↓
Processing happens in the browser
        ↓
Result is generated locally
        ↓
User downloads the result
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no image-processing API sitting behind the upload button.&lt;/p&gt;

&lt;p&gt;The image doesn't need to travel to my server just to be resized or compressed.&lt;/p&gt;

&lt;p&gt;That sounds obvious, but building it reliably across browsers is where things get interesting.&lt;/p&gt;




&lt;h2&gt;
  
  
  Browser APIs are more capable than I initially expected
&lt;/h2&gt;

&lt;p&gt;The foundation is the browser's image and canvas APIs.&lt;/p&gt;

&lt;p&gt;For example, a typical image workflow can involve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;File&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Blob&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;createImageBitmap()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Canvas&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;drawImage()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;canvas.toBlob()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;createImageBitmap()&lt;/code&gt; can also be used from a Web Worker, which makes it useful when moving heavier image work away from the main UI thread.&lt;/p&gt;

&lt;p&gt;That gives us a workflow roughly like this:&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;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;canvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OffscreenCanvas&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;targetHeight&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;context&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="s2"&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;context&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="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;targetHeight&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The actual implementation needs considerably more work than this, especially when dealing with different formats, EXIF orientation, browser differences, memory usage, and cancellation.&lt;/p&gt;

&lt;p&gt;But the underlying concept is surprisingly small.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Web Workers matter
&lt;/h2&gt;

&lt;p&gt;Image processing can become expensive very quickly.&lt;/p&gt;

&lt;p&gt;A 4000×3000 image contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;12,000,000 pixels
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And each pixel may require several bytes of memory depending on the representation being used.&lt;/p&gt;

&lt;p&gt;If all of that work happens directly on the main thread, the UI can become unresponsive.&lt;/p&gt;

&lt;p&gt;So I moved the heavier processing into Web Workers wherever practical.&lt;/p&gt;

&lt;p&gt;The simplified architecture looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                Main Thread
                     │
              user interaction
                     │
                     ▼
              processing request
                     │
                     ▼
              ┌─────────────┐
              │ Web Worker  │
              │             │
              │ decode      │
              │ resize      │
              │ crop        │
              │ encode      │
              └──────┬──────┘
                     │
                     ▼
                result Blob
                     │
                     ▼
                Main Thread
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This also makes cancellation possible.&lt;/p&gt;

&lt;p&gt;If the user starts processing a large image and changes their mind, the worker can be terminated rather than leaving an expensive operation running in the background.&lt;/p&gt;




&lt;h2&gt;
  
  
  The first problem: "compress to 100 KB" isn't as simple as it sounds
&lt;/h2&gt;

&lt;p&gt;One of the tools I wanted was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Compress this image to a maximum of 100 KB.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At first glance, this sounds like:&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="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="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But image quality isn't a direct mapping to output file size.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;quality = 80
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;doesn't mean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;output = 80% of original size
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The actual result depends on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;image dimensions&lt;/li&gt;
&lt;li&gt;image content&lt;/li&gt;
&lt;li&gt;encoder implementation&lt;/li&gt;
&lt;li&gt;browser&lt;/li&gt;
&lt;li&gt;output format&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I implemented a search strategy instead.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Try quality 92
      ↓
Too large?
      ↓
Try quality 40
      ↓
Still too large?
      ↓
Target impossible at these dimensions
      ↓
Otherwise binary-search the quality range
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is that the tool measures the &lt;strong&gt;actual generated Blob size&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It doesn't estimate the result.&lt;/p&gt;

&lt;p&gt;If the user asks for 200 KB, the application checks the actual bytes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Then I discovered an important edge case
&lt;/h2&gt;

&lt;p&gt;Suppose the image is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3200 × 2400
123 KB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the user asks for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Maximum file size: 200 KB
Maximum dimensions: 1200 × 1200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The file is already below the requested file size.&lt;/p&gt;

&lt;p&gt;A naive implementation might say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Great. We're already below 200 KB. Return the original file.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But that would violate the dimension requirement.&lt;/p&gt;

&lt;p&gt;The correct result is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3200 × 2400
        ↓
1200 × 900
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;even though the original file was already small enough.&lt;/p&gt;

&lt;p&gt;This was actually a bug I found while extending Pixyro.&lt;/p&gt;

&lt;p&gt;The fix was conceptually simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Return original file only when:

file size &amp;lt;= target
AND
dimensions &amp;lt;= limits
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;not merely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;file size &amp;lt;= target
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is one of those bugs that is easy to miss if you only test the happy path.&lt;/p&gt;




&lt;h2&gt;
  
  
  Keeping aspect ratio is another constraint
&lt;/h2&gt;

&lt;p&gt;When applying maximum dimensions, I don't want to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;stretch the image&lt;/li&gt;
&lt;li&gt;crop it&lt;/li&gt;
&lt;li&gt;add padding&lt;/li&gt;
&lt;li&gt;change its aspect ratio&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the scale factor is based on the smaller of the two dimension constraints:&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;const&lt;/span&gt; &lt;span class="nx"&gt;scale&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;maxWidth&lt;/span&gt; &lt;span class="o"&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;maxHeight&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="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&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;const&lt;/span&gt; &lt;span class="nx"&gt;newWidth&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;width&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;newHeight&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;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The final implementation also makes sure rounding doesn't accidentally exceed the configured limits.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Original:
3200 × 2400

Maximum:
1200 × 1200

Result:
1200 × 900
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a portrait image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Original:
2400 × 3200

Maximum:
1200 × 1200

Result:
900 × 1200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No cropping.&lt;/p&gt;

&lt;p&gt;No stretching.&lt;/p&gt;

&lt;p&gt;Just proportional scaling.&lt;/p&gt;




&lt;h2&gt;
  
  
  EXIF orientation makes things more interesting
&lt;/h2&gt;

&lt;p&gt;Modern phones can store an image physically as one orientation while using EXIF metadata to tell software how it should be displayed.&lt;/p&gt;

&lt;p&gt;That means the raw dimensions aren't always the dimensions the user thinks they're looking at.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Stored pixels:
4000 × 3000

Displayed orientation:
3000 × 4000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you ignore the orientation metadata, you can easily apply the wrong dimension constraints.&lt;/p&gt;

&lt;p&gt;So image processing needs to account for orientation before making decisions about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;width&lt;/li&gt;
&lt;li&gt;height&lt;/li&gt;
&lt;li&gt;crop&lt;/li&gt;
&lt;li&gt;resize&lt;/li&gt;
&lt;li&gt;preview&lt;/li&gt;
&lt;li&gt;output&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is especially important for photos coming directly from mobile devices.&lt;/p&gt;




&lt;h2&gt;
  
  
  Browser differences are real
&lt;/h2&gt;

&lt;p&gt;Another lesson from building Pixyro:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Don't assume that browser image encoders behave identically.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The same image and nominal quality setting can produce different output sizes in different browsers.&lt;/p&gt;

&lt;p&gt;That matters a lot when the user asks for an exact constraint such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;≤ 200 KB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application can't simply assume that a quality value produces a particular file size.&lt;/p&gt;

&lt;p&gt;It has to inspect the actual result.&lt;/p&gt;

&lt;p&gt;This is also why I prefer honest failure over pretending that every target is achievable.&lt;/p&gt;




&lt;h2&gt;
  
  
  What happens when the target is impossible?
&lt;/h2&gt;

&lt;p&gt;Imagine the user asks for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10 KB
1200 × 1200 maximum dimensions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But even the lowest acceptable quality at those dimensions produces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;29.8 KB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are two bad approaches.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bad approach #1
&lt;/h3&gt;

&lt;p&gt;Keep reducing the dimensions automatically until the file reaches 10 KB.&lt;/p&gt;

&lt;p&gt;That violates the user's dimension constraint.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bad approach #2
&lt;/h3&gt;

&lt;p&gt;Return a file and pretend the target was achieved.&lt;/p&gt;

&lt;p&gt;Obviously worse.&lt;/p&gt;

&lt;p&gt;Instead, the tool tells the user that the target couldn't be reached at the requested dimensions.&lt;/p&gt;

&lt;p&gt;The actual result can still be offered as a secondary option when it is smaller than the original.&lt;/p&gt;

&lt;p&gt;The important principle is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Never hide a failed constraint.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If a requirement wasn't achieved, tell the user.&lt;/p&gt;




&lt;h2&gt;
  
  
  Privacy was a design constraint, not a marketing feature
&lt;/h2&gt;

&lt;p&gt;I didn't want "privacy" to be something added to the homepage after the application was finished.&lt;/p&gt;

&lt;p&gt;It influenced the architecture from the beginning.&lt;/p&gt;

&lt;p&gt;For the image-processing tools, the intended flow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your file
   ↓
Your browser
   ↓
Pixyro processing
   ↓
Your browser
   ↓
Your download
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your file
   ↓
Pixyro server
   ↓
Processing server
   ↓
Storage
   ↓
Your download
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This has another advantage besides privacy:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Infrastructure becomes much simpler.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There is no image-processing backend to scale.&lt;/p&gt;

&lt;p&gt;No upload storage.&lt;/p&gt;

&lt;p&gt;No cleanup jobs for uploaded images.&lt;/p&gt;

&lt;p&gt;No queue for ordinary image conversions.&lt;/p&gt;

&lt;p&gt;For a free tools website, that matters.&lt;/p&gt;




&lt;h2&gt;
  
  
  But browser-only doesn't mean "problem solved"
&lt;/h2&gt;

&lt;p&gt;There are trade-offs.&lt;/p&gt;

&lt;p&gt;Large images can consume significant memory.&lt;/p&gt;

&lt;p&gt;Different browsers have different capabilities.&lt;/p&gt;

&lt;p&gt;Some formats require specialized decoders.&lt;/p&gt;

&lt;p&gt;Some browsers can't encode certain formats.&lt;/p&gt;

&lt;p&gt;For example, WebP output isn't available in every browser environment in the same way, so the application needs to handle unsupported cases explicitly rather than silently changing the requested format.&lt;/p&gt;

&lt;p&gt;That's an important UX principle for tools:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Don't silently change the user's requested output.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If they asked for WebP and the browser can't produce WebP, say so.&lt;/p&gt;




&lt;h2&gt;
  
  
  HEIC was a completely different problem
&lt;/h2&gt;

&lt;p&gt;HEIC files were more complicated than JPEG, PNG, or WebP.&lt;/p&gt;

&lt;p&gt;For Pixyro, I didn't want to solve HEIC by simply uploading the image to an external conversion API.&lt;/p&gt;

&lt;p&gt;Instead, I built a dedicated WebAssembly-based decoding path.&lt;/p&gt;

&lt;p&gt;The architecture becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HEIC
 ↓
HEIC Web Worker
 ↓
WASM decoder
 ↓
decoded pixels
 ↓
existing image pipeline
 ↓
JPEG Blob
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The decoder is loaded only when the HEIC functionality is actually needed.&lt;/p&gt;

&lt;p&gt;That keeps the normal image tools lightweight.&lt;/p&gt;

&lt;p&gt;It also means that a user opening the regular JPEG compressor doesn't need to download a HEIC decoder.&lt;/p&gt;




&lt;h2&gt;
  
  
  The application became less about "image tools"
&lt;/h2&gt;

&lt;p&gt;This was probably the biggest lesson from the project.&lt;/p&gt;

&lt;p&gt;At the beginning, I was thinking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I want to build a website with a bunch of image tools."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But that isn't particularly interesting.&lt;/p&gt;

&lt;p&gt;There are already hundreds of websites that can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;resize images&lt;/li&gt;
&lt;li&gt;compress images&lt;/li&gt;
&lt;li&gt;convert formats&lt;/li&gt;
&lt;li&gt;create PDFs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The more interesting question became:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Can a browser-based tool solve a very specific file requirement correctly?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Compress this image to ≤ 100 KB.

AND

Keep it below 1200 × 1200.

AND

Don't stretch it.

AND

Don't upload it.

AND

Tell me honestly if the requirement is impossible.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's much more specific than:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Compress image."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And I think this is where useful web tools can still differentiate themselves.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing became almost as important as the feature
&lt;/h2&gt;

&lt;p&gt;For these tools, a UI test isn't enough.&lt;/p&gt;

&lt;p&gt;I need to verify the actual downloaded file.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Input:
3200 × 2400 JPEG

Configuration:
1200 × 1200
200 KB

Expected:
≤ 200 KB
1200 × 900
JPEG
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the test suite checks things such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;actual output dimensions&lt;/li&gt;
&lt;li&gt;actual output file size&lt;/li&gt;
&lt;li&gt;MIME type&lt;/li&gt;
&lt;li&gt;image orientation&lt;/li&gt;
&lt;li&gt;aspect ratio&lt;/li&gt;
&lt;li&gt;transparency behavior&lt;/li&gt;
&lt;li&gt;cancellation&lt;/li&gt;
&lt;li&gt;browser differences&lt;/li&gt;
&lt;li&gt;responsive behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the latest version of the target-size compressor, I ended up testing the production implementation across Chrome, Firefox and WebKit, including mobile browser environments.&lt;/p&gt;

&lt;p&gt;That caught a few things that ordinary unit tests alone wouldn't have exposed.&lt;/p&gt;




&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;p&gt;The application is built with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Next.js&lt;/li&gt;
&lt;li&gt;React&lt;/li&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;Tailwind CSS&lt;/li&gt;
&lt;li&gt;Web Workers&lt;/li&gt;
&lt;li&gt;Canvas APIs&lt;/li&gt;
&lt;li&gt;WebAssembly where necessary&lt;/li&gt;
&lt;li&gt;Cloudflare Pages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The interesting part isn't really the framework.&lt;/p&gt;

&lt;p&gt;The interesting part is keeping the processing architecture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser-first
Worker-based where appropriate
Reusable
Format-aware
Constraint-aware
Privacy-conscious
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What I'm building next
&lt;/h2&gt;

&lt;p&gt;Pixyro is now live as a collection of free browser-based tools.&lt;/p&gt;

&lt;p&gt;But I'm deliberately &lt;strong&gt;not&lt;/strong&gt; trying to add 50 more tools immediately.&lt;/p&gt;

&lt;p&gt;I'm more interested in answering a different question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What exact file problems are people actually trying to solve?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For example, someone might not really want an "image compressor."&lt;/p&gt;

&lt;p&gt;They might need:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"A 3000×2000 photo reduced to less than 200 KB without making the text unreadable."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's a very different product requirement.&lt;/p&gt;

&lt;p&gt;And that's the direction I'm exploring.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;Building browser-based image processing taught me that the hard part isn't putting a file input and a canvas on a page.&lt;/p&gt;

&lt;p&gt;The hard part is handling the edge cases honestly.&lt;/p&gt;

&lt;p&gt;Things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;EXIF orientation&lt;/li&gt;
&lt;li&gt;browser-specific encoders&lt;/li&gt;
&lt;li&gt;large image memory usage&lt;/li&gt;
&lt;li&gt;exact file-size constraints&lt;/li&gt;
&lt;li&gt;dimension constraints&lt;/li&gt;
&lt;li&gt;cancellation&lt;/li&gt;
&lt;li&gt;unsupported formats&lt;/li&gt;
&lt;li&gt;privacy&lt;/li&gt;
&lt;li&gt;and impossible requirements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The browser is capable of doing much more image processing than I originally expected.&lt;/p&gt;

&lt;p&gt;And for many use cases, the best architecture might be:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't upload the file at all.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Process it where it already exists.&lt;/p&gt;

&lt;p&gt;In the user's browser.&lt;/p&gt;




&lt;p&gt;I'm building Pixyro as a practical experiment around that idea.&lt;/p&gt;

&lt;p&gt;If you're building browser-based file or image processing tools yourself, I'd be interested to hear:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What edge case caused you the most trouble?&lt;/strong&gt;&lt;/p&gt;

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