<?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: Mahendra Singh deora </title>
    <description>The latest articles on DEV Community by Mahendra Singh deora  (@viksa7726).</description>
    <link>https://dev.to/viksa7726</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%2F4190593%2F53096fd3-53ad-49ab-b57f-4cdeadab732e.png</url>
      <title>DEV Community: Mahendra Singh deora </title>
      <link>https://dev.to/viksa7726</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/viksa7726"/>
    <language>en</language>
    <item>
      <title>How I built a photo &amp; signature cropper that never uploads your files</title>
      <dc:creator>Mahendra Singh deora </dc:creator>
      <pubDate>Sat, 17 Oct 2026 13:58:07 +0000</pubDate>
      <link>https://dev.to/viksa7726/how-i-built-a-photo-signature-cropper-that-never-uploads-your-files-2b72</link>
      <guid>https://dev.to/viksa7726/how-i-built-a-photo-signature-cropper-that-never-uploads-your-files-2b72</guid>
      <description>&lt;p&gt;Online forms are picky. A passport photo must be 2×2 inches. A signature must sit under 20 KB. A school office has one scanned sheet with thirty student photos and needs thirty clean files by tomorrow.&lt;/p&gt;

&lt;p&gt;Most “free” tools solve that by uploading your ID photo to a server. I did not want that for Crop Desk, so every tool on &lt;a href="https://cropdesk.co.in" rel="noopener noreferrer"&gt;cropdesk.co.in&lt;/a&gt; runs in the browser. The sheet, the crop, the ZIP, and the JPEG encode all stay on the device.&lt;/p&gt;

&lt;p&gt;This post walks through three pieces: canvas cropping, sheet auto-detection with thresholding and connected components, and hitting an exact KB target with a binary search on JPEG quality. The rest of the toolkit (PDF merge/split/compress, image↔PDF, format conversion, age calculator) follows the same privacy model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why client-side only
&lt;/h2&gt;

&lt;p&gt;The data path is intentionally short:&lt;/p&gt;

&lt;p&gt;File → &lt;code&gt;FileReader&lt;/code&gt; / &lt;code&gt;createImageBitmap&lt;/code&gt; → draw on &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; → &lt;code&gt;canvas.toBlob&lt;/code&gt; → download or JSZip.&lt;/p&gt;

&lt;p&gt;There is no upload step in the happy path. After the page loads, the tools keep working offline. Ads pay for hosting; there is no account and no “pro” paywall for basic crops.&lt;/p&gt;

&lt;p&gt;That constraint shaped the algorithms. No cloud vision API for box detection. No server-side ImageMagick for compression. Everything has to finish in a second or two on a mid-range Android phone, because that is where a lot of form uploads actually happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Canvas cropping (the boring foundation)
&lt;/h2&gt;

&lt;p&gt;Every crop is the same pattern:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Load the image into an &lt;code&gt;HTMLImageElement&lt;/code&gt; or &lt;code&gt;ImageBitmap&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Create a canvas sized to the &lt;strong&gt;output&lt;/strong&gt; pixels (e.g. 600×600 for a US 2×2 at 300 DPI, or whatever the preset needs).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;drawImage(source, sx, sy, sw, sh, 0, 0, dw, dh)&lt;/code&gt; to copy the selected region and scale it in one call.&lt;/li&gt;
&lt;li&gt;Export with &lt;code&gt;toBlob('image/jpeg', quality)&lt;/code&gt; or PNG/WebP.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Presets (US 2×2, 35×45 mm, Instagram, YouTube thumbnails, and similar) are mostly metadata: target width/height, optional print DPI label written into the file, and UI guides such as head-size lines for passport photos. The crop math does not care which portal you are filling — only the pixels you ask for.&lt;/p&gt;

&lt;p&gt;For print-oriented sizes, convert millimetres or inches to pixels with &lt;code&gt;px = mm / 25.4 * dpi&lt;/code&gt; (or inches × dpi). DPI itself does not change pixel data; some forms still check the EXIF/JFIF density tag, so a small metadata pass helps without re-encoding quality.&lt;/p&gt;

&lt;p&gt;Signature background removal is the same canvas pipeline with an extra pixel pass: after drawing the crop, treat near-white (or near-paper) samples as background — either fill them white for JPG forms, or set alpha to 0 when exporting PNG.&lt;/p&gt;

&lt;h2&gt;
  
  
  Auto-detecting photos and signatures on a sheet
&lt;/h2&gt;

&lt;p&gt;The bulk tool is for a common real workflow: one A4 scan (or phone photo of a sheet) with a grid of passport photos and signature boxes. Manual cropping per person is slow and easy to mis-number when you are preparing a whole class.&lt;/p&gt;

&lt;p&gt;Rough pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Downscale&lt;/strong&gt; a working copy so luminance and labeling loops stay cheap (detection runs on the small image; crops still use full resolution).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Threshold&lt;/strong&gt; to a binary mask. Scanned forms are usually dark ink/photos on light paper, so a luminance threshold — or a simple adaptive threshold when lighting is uneven — separates “ink/photo” from background.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connected components&lt;/strong&gt; on the foreground mask (4-connectivity via union-find or two-pass labeling). Each blob gets a bounding box and area.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filter&lt;/strong&gt; dust, staple marks, and full-page borders using minimum area and aspect-ratio bounds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Classify&lt;/strong&gt; remaining boxes: larger, squarer regions tend to be photos; wider, shorter ones tend to be signatures. Layout heuristics (Sheet A: signature above photo; Sheet B: photo above signature) pair them and sort in reading order (top-to-bottom, then left-to-right within a row).&lt;/li&gt;
&lt;li&gt;Draw green / dashed overlays so the user can nudge one box or all boxes, or scale them slightly, before export.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Edge cases are honest limitations: skewed phone photos, heavy shadows, patterned table backgrounds, and unusual form layouts. For those, Blank mode lets you draw boxes, add a grid, or reuse a saved layout from last week’s batch. Detection is a helper, not a guarantee.&lt;/p&gt;

&lt;p&gt;When you confirm, each box is mapped back to full-resolution coordinates, cropped on a canvas, and named in order: &lt;code&gt;1_photo.jpg&lt;/code&gt;, &lt;code&gt;1_sign.jpg&lt;/code&gt;, &lt;code&gt;2_photo.jpg&lt;/code&gt;, … then packed with JSZip in the browser. Numbers follow box order on the sheet, so a numbered attendance list stays aligned with the ZIP.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compressing to an exact KB target
&lt;/h2&gt;

&lt;p&gt;Upload portals say “photo max 50 KB,” not “quality 0.72.” JPEG size is not linear in the quality slider, but it &lt;strong&gt;is&lt;/strong&gt; monotonic at fixed dimensions: higher quality never produces a smaller file. That makes binary search a clean fit.&lt;/p&gt;

&lt;p&gt;Sketch of the approach used in the compressor:&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;fitQuality&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;targetBytes&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;floorQ&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.3&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;ceilQ&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.95&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;atCeil&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;canvasToJpeg&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;ceilQ&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;atCeil&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;targetBytes&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;atCeil&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;atFloor&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;canvasToJpeg&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;floorQ&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;atFloor&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;gt;&lt;/span&gt; &lt;span class="nx"&gt;targetBytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// need smaller dimensions&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="nx"&gt;floorQ&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="nx"&gt;ceilQ&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;atFloor&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;mid&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;canvasToJpeg&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;mid&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;targetBytes&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;mid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// try higher quality&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&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="nx"&gt;mid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// too big — lower quality&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;canvasToJpeg&lt;/code&gt; is just &lt;code&gt;canvas.toBlob(..., 'image/jpeg', q)&lt;/code&gt; wrapped in a Promise. Eight iterations are enough to land close to the best quality that still fits; more iterations give diminishing returns because the encoder’s quality scale is coarse.&lt;/p&gt;

&lt;p&gt;If even floor quality is too big, quality alone cannot win. Encoded size scales roughly with pixel area, so the fallback shrinks width and height by about &lt;code&gt;sqrt(targetBytes / currentBytes)&lt;/code&gt;, redraws onto a smaller canvas, and runs the quality search again. Landing a little under the target is normal and desirable — portals accept “≤ 50 KB,” and JPEG entropy depends on how detailed the picture is.&lt;/p&gt;

&lt;p&gt;The same pattern backs “resize and stay under N KB” flows: lock the required pixel size first when the form demands it, search quality, and only shrink further if the floor still overshoots.&lt;/p&gt;

&lt;h2&gt;
  
  
  PDFs and the rest of the toolkit
&lt;/h2&gt;

&lt;p&gt;Not every rejection is about photos. PDF merge, split, rearrange, rotate, and compress use client-side PDF libraries with the same privacy model: parse in memory, rewrite pages, download. Image→PDF draws canvas pages into a PDF; PDF→image renders pages back out. JPG/PNG/WebP conversion is decode → canvas → encode (with a chosen background fill when flattening transparency to JPG). An age calculator sits alongside for forms that ask for exact age on a reference date.&lt;/p&gt;

&lt;p&gt;None of that requires your files on my server. Practical write-ups for passport sizes, KB limits, signatures, and HEIC quirks live at &lt;a href="https://cropdesk.co.in/guides/" rel="noopener noreferrer"&gt;cropdesk.co.in/guides&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I am not claiming
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Auto-detect will not nail every crumpled phone photo or fancy coloured form.&lt;/li&gt;
&lt;li&gt;Hitting “exactly 50.0 KB” is less useful than “highest quality under 50 KB.”&lt;/li&gt;
&lt;li&gt;Crop Desk is not affiliated with any exam board, university, or government portal — always check the official notification for sizes and formats.&lt;/li&gt;
&lt;li&gt;The site is free and ad-supported; ads are how hosting stays paid without a sign-up wall.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;If you prepare form uploads, run a cyber café / CSC desk, teach a class that submits photos in bulk, or just hate uploading ID photos to random compressors, try the tools at &lt;strong&gt;&lt;a href="https://cropdesk.co.in" rel="noopener noreferrer"&gt;https://cropdesk.co.in&lt;/a&gt;&lt;/strong&gt;. Feedback on failed sheet detections, missing country presets, and PDF edge cases is welcome — those reports steer what gets fixed next.&lt;/p&gt;

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