<?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: Devanshu Verma</title>
    <description>The latest articles on DEV Community by Devanshu Verma (@devanshu_verma).</description>
    <link>https://dev.to/devanshu_verma</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%2F2984386%2F6633aa96-f8ec-45fb-8069-9d8ee51bf992.jpeg</url>
      <title>DEV Community: Devanshu Verma</title>
      <link>https://dev.to/devanshu_verma</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/devanshu_verma"/>
    <language>en</language>
    <item>
      <title>Shipping a Mac app without a $99 Apple Developer account</title>
      <dc:creator>Devanshu Verma</dc:creator>
      <pubDate>Wed, 21 Oct 2026 19:15:24 +0000</pubDate>
      <link>https://dev.to/devanshu_verma/shipping-a-mac-app-without-a-99-apple-developer-account-1b6j</link>
      <guid>https://dev.to/devanshu_verma/shipping-a-mac-app-without-a-99-apple-developer-account-1b6j</guid>
      <description>&lt;p&gt;I build &lt;a href="https://kopi.devanshuverma.in" rel="noopener noreferrer"&gt;Kopi&lt;/a&gt;, a free, open-source (MIT) clipboard manager for macOS and Windows. It's written in Rust with Tauri 2 and React, and it's in alpha.&lt;/p&gt;

&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%2F3sq3stfzg9oz54ttavkv.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%2F3sq3stfzg9oz54ttavkv.png" alt="Kopi's clipboard panel on macOS" width="800" height="505"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This post is about one decision that took more thought than any feature: how to ship a Mac app without paying Apple $99 a year. I wrote it up as a decision record in the repo (&lt;a href="https://github.com/code-devanshu/kopi/blob/main/docs/decisions/0006-macos-distribution-without-notarization.md" rel="noopener noreferrer"&gt;ADR 0006&lt;/a&gt;), and this is the longer story behind it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: $99 a year to open without a warning
&lt;/h2&gt;

&lt;p&gt;On macOS, an app downloaded from the internet opens without a Gatekeeper warning only if it is signed with an Apple Developer ID certificate and notarized by Apple. Getting that certificate means joining the Apple Developer Program, which costs $99 a year.&lt;/p&gt;

&lt;p&gt;For a commercial app that's a normal cost of doing business. Kopi is free and open source, and I decided it won't pay that fee every year. So the question became: what's the best Mac experience I can ship without it?&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "just don't sign it" doesn't work
&lt;/h2&gt;

&lt;p&gt;My first thought was the obvious one. Skip the paid certificate, sign the app ad-hoc (which is what you get by default when you build locally), and tell people to click through the warning.&lt;/p&gt;

&lt;p&gt;That falls apart because of what Kopi does. A clipboard manager needs two macOS permissions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Accessibility&lt;/strong&gt;, so it can paste into the app you're using.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pasteboard access&lt;/strong&gt; ("Paste from Other Apps"), so it can see what you copy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;macOS doesn't remember these grants by app name. It ties them to the app's &lt;strong&gt;designated requirement&lt;/strong&gt;, a rule baked into the code signature that says "this is the same app." For an ad-hoc signature, that requirement includes the build's own hash. Every new build has a new hash, so every new build looks like a different app to macOS.&lt;/p&gt;

&lt;p&gt;In practice that meant my alpha testers lost both permissions on every update. Paste stopped working, history stopped recording, and they had to dig into System Settings and re-grant everything. For an app that ships frequent alpha updates, that's not something I could ask people to put up with.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: a self-signed certificate that never changes
&lt;/h2&gt;

&lt;p&gt;The key insight is that the designated requirement doesn't have to mention the build's hash. If you sign with a real certificate, even a self-signed one, the requirement can be based on the bundle identifier and the certificate instead.&lt;/p&gt;

&lt;p&gt;So I created a self-signed code-signing certificate and stored it in the repository's secrets. Every release is signed with it. The resulting designated requirement looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;identifier "in.devanshuverma.kopi" and certificate leaf = H"…"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the bundle identifier plus the hash of the certificate's leaf. Neither changes between builds. To macOS, version N+1 is the same app as version N, so the Accessibility and pasteboard grants carry over.&lt;/p&gt;

&lt;p&gt;This doesn't make Gatekeeper happy. A self-signed certificate isn't trusted by Apple, so the app is still blocked on first open. But it solves the much worse problem of permissions resetting on every update.&lt;/p&gt;

&lt;p&gt;You can check this yourself on any signed app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;codesign &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-r-&lt;/span&gt; /Applications/Kopi.app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  One snag with Tauri
&lt;/h3&gt;

&lt;p&gt;Tauri's bundler can sign macOS builds for you if you hand it a certificate through the &lt;code&gt;APPLE_CERTIFICATE&lt;/code&gt; environment variable. The catch: its import only finds Apple-issued certificates, and it refuses to build a release without one. A self-signed certificate isn't Apple-issued, so that path doesn't work.&lt;/p&gt;

&lt;p&gt;The workaround is to have the release workflow import the certificate into the keychain itself, then let the build sign with that identity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accepting the first-open block, and making it painless
&lt;/h2&gt;

&lt;p&gt;With a self-signed certificate, Gatekeeper will block Kopi the first time someone opens it. I decided to accept that and put the effort into making the first launch as painless as I could, in three ways.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An install guide.&lt;/strong&gt; &lt;a href="https://github.com/code-devanshu/kopi/blob/main/docs/INSTALL_ALPHA.md" rel="noopener noreferrer"&gt;INSTALL_ALPHA.md&lt;/a&gt; walks through it step by step. On macOS 15 Sequoia and later, you allow Kopi in System Settings → Privacy &amp;amp; Security. On macOS 14 Sonoma, you Control-click the app and choose Open. The guide also explains each permission and what happens if you don't grant it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A .dmg background that shows the steps.&lt;/strong&gt; Most people never read install docs, but everyone sees the disk image window when they drag the app to Applications. So the .dmg background itself shows what to do after the first block.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A one-line Terminal install.&lt;/strong&gt; For people comfortable with Terminal, there's a script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://kopi.devanshuverma.in/install.sh | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It downloads Kopi, verifies the download, and copies the app to Applications. The script doesn't quarantine the app, so Gatekeeper doesn't block the first launch.&lt;/p&gt;

&lt;p&gt;Piping a script into bash from the internet deserves some care, so the script has a guardrail: it only installs a Kopi that is signed with Kopi's certificate. If the signature doesn't match, it doesn't install. As always, you can read the script before running it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Updates: blocked once, never again
&lt;/h2&gt;

&lt;p&gt;The first install is the only time Gatekeeper gets in the way. After that, Kopi updates itself with the Tauri updater.&lt;/p&gt;

&lt;p&gt;The updater signs updates with its own key, separate from the code-signing certificate. It installs updates without quarantine, so Gatekeeper doesn't block them. And because every build carries the same designated requirement, the permissions survive too.&lt;/p&gt;

&lt;p&gt;So the full experience for a tester is: one "Open Anyway" on day one, grant two permissions, and then updates just arrive. Kopi checks every few hours and installs in the background, and you can turn automatic updates off in Settings.&lt;/p&gt;

&lt;h2&gt;
  
  
  The options I rejected
&lt;/h2&gt;

&lt;p&gt;Here's what else I considered and why I didn't go with it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Why not&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Developer ID + notarization&lt;/td&gt;
&lt;td&gt;Removes the first-launch warning, but costs $99 every year.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ad-hoc signing only&lt;/td&gt;
&lt;td&gt;Free, but Accessibility and pasteboard permissions reset on every update.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mac App Store&lt;/td&gt;
&lt;td&gt;Needs the same paid program, and Kopi can't run in the App Sandbox.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Homebrew cask&lt;/td&gt;
&lt;td&gt;The official tap disabled casks that fail Gatekeeper on 2026-09-01, and &lt;code&gt;--no-quarantine&lt;/code&gt; is gone. A personal tap could work, but it would have to clear the quarantine flag itself. Deferred unless testers ask for it.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The honest trade-offs
&lt;/h2&gt;

&lt;p&gt;This approach is not free of costs. They just aren't monetary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every new user has to find "Open Anyway".&lt;/strong&gt; It lives in System Settings, and macOS 15 made it harder to get to than the old Control-click → Open trick. The install guide, the .dmg background and the Terminal script all exist to soften this, but it's still a step a notarized app wouldn't need.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Some people won't install an app macOS warns about.&lt;/strong&gt; That's a reasonable instinct, and I'm not going to argue anyone out of it. Kopi being open source means you can read the code and build it yourself, but I accept that some people will close the warning and move on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The certificate's private key has to be guarded.&lt;/strong&gt; Anything signed with that key is something macOS will treat as Kopi, including inheriting the permissions people granted. That's why it lives in the repo's secrets. If it were ever lost, I'd have to create a new certificate, and every user would have to re-grant their permissions once.&lt;/p&gt;

&lt;p&gt;For a free, alpha-stage app, I think that's the right trade. If Kopi grows to the point where the first-launch warning is the main thing holding people back, I'll revisit it. That's what the decision record is for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try Kopi
&lt;/h2&gt;

&lt;p&gt;Kopi is open source under the MIT license and in alpha on macOS (14 Sonoma or later, Apple silicon or Intel) and Windows 10/11. It keeps your clipboard history locally with no account, filters out password-manager content, and has optional end-to-end encrypted sync over your local network.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Website: &lt;a href="https://kopi.devanshuverma.in" rel="noopener noreferrer"&gt;kopi.devanshuverma.in&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/code-devanshu/kopi" rel="noopener noreferrer"&gt;github.com/code-devanshu/kopi&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you try it, I'd love to hear what breaks. Issues on GitHub are the best place for bug reports, and Windows feedback is especially welcome right now.&lt;/p&gt;

</description>
      <category>macos</category>
      <category>rust</category>
      <category>opensource</category>
      <category>tauri</category>
    </item>
    <item>
      <title>I got tired of sketchy photo resizers, so I built one that never uploads your photo</title>
      <dc:creator>Devanshu Verma</dc:creator>
      <pubDate>Wed, 07 Oct 2026 13:00:51 +0000</pubDate>
      <link>https://dev.to/devanshu_verma/i-got-tired-of-sketchy-photo-resizers-so-i-built-one-that-never-uploads-your-photo-4971</link>
      <guid>https://dev.to/devanshu_verma/i-got-tired-of-sketchy-photo-resizers-so-i-built-one-that-never-uploads-your-photo-4971</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%2Fformpic.devanshuverma.in%2Fopengraph-image" 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%2Fformpic.devanshuverma.in%2Fopengraph-image" alt="FormPic: crop and resize photos for forms" width="1200" height="630"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you've applied for anything in India (a passport, a PAN card, UPSC, NEET, a bank exam), you've seen some version of this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Upload photograph: 35×45 mm, JPEG, under 50 KB."&lt;br&gt;
"Upload signature: 60×20 mm, 10 to 20 KB."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You crop it in some app, export it, upload it, and the portal says no. Wrong dimensions. Or 53 KB. Try again.&lt;/p&gt;

&lt;p&gt;The usual fix is to search "photo resizer online" and pick whichever site loads first. Those sites are covered in ads, and most of them upload your photo to a server. It's your face, going into an ID application, sitting on a machine you know nothing about. That bugged me enough that I built my own: &lt;a href="https://formpic.devanshuverma.in/" rel="noopener noreferrer"&gt;FormPic&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;You drop in a photo, pick the form, drag the crop box over your face and download. The file comes out at the exact pixel size the form wants and under the file size limit you set.&lt;/p&gt;

&lt;p&gt;The part I care about most: the photo never leaves your browser. There's no upload step at all. It's free, there's no account, and there's no watermark. Once the page has loaded it keeps working offline, and you can take the photo with your phone or laptop camera instead of uploading one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it here: &lt;a href="https://formpic.devanshuverma.in/" rel="noopener noreferrer"&gt;formpic.devanshuverma.in&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The sizes it knows about
&lt;/h2&gt;

&lt;p&gt;These are the presets right now:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Document&lt;/th&gt;
&lt;th&gt;Print size&lt;/th&gt;
&lt;th&gt;Pixels&lt;/th&gt;
&lt;th&gt;Resolution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Passport size&lt;/td&gt;
&lt;td&gt;35×45 mm&lt;/td&gt;
&lt;td&gt;413 × 531 px&lt;/td&gt;
&lt;td&gt;300 DPI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;US passport &amp;amp; visa&lt;/td&gt;
&lt;td&gt;2×2 in&lt;/td&gt;
&lt;td&gt;600 × 600 px&lt;/td&gt;
&lt;td&gt;300 DPI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PAN card&lt;/td&gt;
&lt;td&gt;25×35 mm&lt;/td&gt;
&lt;td&gt;197 × 276 px&lt;/td&gt;
&lt;td&gt;200 DPI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UPSC&lt;/td&gt;
&lt;td&gt;35×45 mm&lt;/td&gt;
&lt;td&gt;413 × 531 px&lt;/td&gt;
&lt;td&gt;300 DPI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NEET&lt;/td&gt;
&lt;td&gt;35×45 mm&lt;/td&gt;
&lt;td&gt;413 × 531 px&lt;/td&gt;
&lt;td&gt;300 DPI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IBPS&lt;/td&gt;
&lt;td&gt;35×45 mm&lt;/td&gt;
&lt;td&gt;200 × 230 px&lt;/td&gt;
&lt;td&gt;set in pixels&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSC signature&lt;/td&gt;
&lt;td&gt;60×20 mm&lt;/td&gt;
&lt;td&gt;472 × 157 px&lt;/td&gt;
&lt;td&gt;200 DPI&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If your form isn't listed, you can type any width and height in pixels and lock the aspect ratio.&lt;/p&gt;

&lt;p&gt;What makes these annoying is that you have to get the aspect ratio, the pixel dimensions and the file size right at the same time. Most tools I tried handled one of them and left the rest to me.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works without a server
&lt;/h2&gt;

&lt;p&gt;A browser already has everything a photo resizer needs. You read the file, draw the cropped part onto a canvas at the target size, and encode it. With no backend there's nothing to store and nothing to leak, and no reason the tool can't run offline.&lt;/p&gt;

&lt;p&gt;Here's a simplified sketch of the crop step:&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;cropAndResize&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;crop&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="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="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;width&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;height&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;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="c1"&gt;// source rect (the crop box) -&amp;gt; destination rect (the form size)&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="nx"&gt;crop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;crop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;crop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;crop&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="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;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="k"&gt;return&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Getting under 50 KB without guessing
&lt;/h3&gt;

&lt;p&gt;This was the fun bit. JPEG file size doesn't go up in a straight line with quality, so you can't just say "50 KB means quality 0.7." What does work is a binary search over the quality value until you find the highest one that still fits:&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;toBlob&lt;/span&gt; &lt;span class="o"&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;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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="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;resolve&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/jpeg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;));&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;fitToSize&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;maxBytes&lt;/span&gt;&lt;span class="p"&gt;)&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;lo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hi&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="nx"&gt;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&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="mi"&gt;8&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;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;hi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&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="nf"&gt;toBlob&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;q&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;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;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;maxBytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;best&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;lo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nx"&gt;hi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;q&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;best&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// null means it's too big even at the lowest quality&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eight rounds gets you within about 0.4% of the best quality, which is plenty. If the photo is still too big at the lowest quality, there's a "Shrink to fit" option that lowers the pixel size too.&lt;/p&gt;

&lt;p&gt;A few smaller things I spent time on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Picking a form preset switches the output to JPEG, since that's what nearly every portal asks for. PNG is still there if you want a lossless copy to print.&lt;/li&gt;
&lt;li&gt;The preset outlines are drawn to scale, so you can see the shape before you choose.&lt;/li&gt;
&lt;li&gt;The Download button shows the final file size. You find out it's 48 KB before you're stuck on a slow government site at midnight, not after.&lt;/li&gt;
&lt;li&gt;Nothing is kept. Close the tab and the photo is gone.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it doesn't do
&lt;/h2&gt;

&lt;p&gt;It won't change your background to white. I went back and forth on this, but doing it well in the browser is hard, and doing it badly looks worse than a plain wall. For now my advice is to stand in front of a white or light wall with even light. That fixes it most of the time.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few questions I get
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What's a passport photo in pixels?&lt;/strong&gt; 35 × 45 mm at 300 DPI is 413 × 531 px. Some portals want a different upload size, though, so check the form and type its numbers in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I get a photo under 20 KB?&lt;/strong&gt; Pick JPEG, set Max file size to 20, and download.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is it really not uploaded?&lt;/strong&gt; Open DevTools, go to the Network tab and try it. You won't see the image go anywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tell me what's missing
&lt;/h2&gt;

&lt;p&gt;I build and run FormPic on my own, and I add presets based on what people ask for. If your form or country needs a size that's not there, leave a comment with the form name, the size (mm or px) and any file size limit, and I'll add it.&lt;/p&gt;

&lt;p&gt;I'd also like to hear from other devs about the UX, or about how you'd handle background removal client-side without shipping a huge model.&lt;/p&gt;

&lt;p&gt;And if you know someone who's about to fight an exam portal, send them the link. It might save them an evening.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://formpic.devanshuverma.in/" rel="noopener noreferrer"&gt;formpic.devanshuverma.in&lt;/a&gt;&lt;/p&gt;

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