<?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: Sabbir Hossain</title>
    <description>The latest articles on DEV Community by Sabbir Hossain (@sabbirhossain02).</description>
    <link>https://dev.to/sabbirhossain02</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%2F4142089%2F0a1c728a-bfea-4559-8eed-64962cb88874.jpg</url>
      <title>DEV Community: Sabbir Hossain</title>
      <link>https://dev.to/sabbirhossain02</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sabbirhossain02"/>
    <language>en</language>
    <item>
      <title>I built a free image and PDF toolkit that runs entirely in the browser</title>
      <dc:creator>Sabbir Hossain</dc:creator>
      <pubDate>Fri, 25 Sep 2026 00:51:58 +0000</pubDate>
      <link>https://dev.to/sabbirhossain02/i-built-a-free-image-and-pdf-toolkit-that-runs-entirely-in-the-browser-aaa</link>
      <guid>https://dev.to/sabbirhossain02/i-built-a-free-image-and-pdf-toolkit-that-runs-entirely-in-the-browser-aaa</guid>
      <description>&lt;p&gt;Most "free online" image compressors upload your photo to a server, process it there and send it back. That works, but it means a copy of your file sits on someone else's machine, and the site pays for bandwidth and CPU, which usually ends in a daily limit or a watermark.&lt;/p&gt;

&lt;p&gt;I wanted a toolkit where none of that happens. The result is &lt;strong&gt;&lt;a href="https://compresslyonline.blogspot.com/" rel="noopener noreferrer"&gt;Compressly&lt;/a&gt;&lt;/strong&gt;: an image compressor, resizer, converter, background remover, upscaler and a set of PDF and Word tools. All of it runs in the browser tab.&lt;/p&gt;

&lt;p&gt;This post explains how a few of the parts work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compressing to an exact file size
&lt;/h2&gt;

&lt;p&gt;A lot of people don't care about "quality 80". They care about a limit: a job application that accepts photos under 100 KB, or an exam form that wants 50 KB.&lt;/p&gt;

&lt;p&gt;The Canvas API can encode JPEG and WebP at a given quality, but it can't aim for a size. So Compressly runs a binary search over the quality value:&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;encodeToTarget&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;mime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;target&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.1&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="mf"&gt;0.92&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;first&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;encode&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;mime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hi&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;first&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;target&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;first&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// already fits at high quality&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;7&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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;lo&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.015&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;encode&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;mime&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;target&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="c1"&gt;// fits: try higher quality&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="c1"&gt;// too big: go lower&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;// highest quality that fits&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If even the lowest quality is too big, it reduces the pixel dimensions, using the ratio between the target and the current size to estimate the next scale, and searches again. The UI always says whether the target was reached, instead of rounding the number down.&lt;/p&gt;

&lt;p&gt;Two details made this fast enough to feel instant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Encoding runs in a &lt;strong&gt;Web Worker&lt;/strong&gt; with &lt;code&gt;OffscreenCanvas.convertToBlob()&lt;/code&gt;, so a 7-pass search doesn't freeze the page.&lt;/li&gt;
&lt;li&gt;The resized canvas is cached, so moving the quality slider only re-encodes and doesn't resample again.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Smaller PNGs without losing transparency
&lt;/h2&gt;

&lt;p&gt;The browser's PNG encoder is always lossless, so a screenshot saved as PNG stays large. Tools like TinyPNG get their savings by reducing the image to a palette of up to 256 colours.&lt;/p&gt;

&lt;p&gt;Compressly does the same thing in the worker: median-cut quantisation refined with a couple of k-means passes, optional Floyd–Steinberg dithering, and then it writes the indexed PNG itself (IHDR, PLTE, tRNS, IDAT) with &lt;code&gt;CompressionStream('deflate')&lt;/code&gt; for the compression. Quality 96 or above skips the palette and keeps the PNG lossless.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI background removal without a server
&lt;/h2&gt;

&lt;p&gt;The background remover runs a segmentation model in the browser with &lt;a href="https://github.com/huggingface/transformers.js" rel="noopener noreferrer"&gt;Transformers.js&lt;/a&gt;. I chose &lt;a href="https://huggingface.co/onnx-community/ormbg-ONNX" rel="noopener noreferrer"&gt;ormbg&lt;/a&gt; because it is Apache-2.0 licensed; some popular background-removal models don't allow commercial use.&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;T&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;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.3.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;backends&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onnx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wasm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;proxy&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="c1"&gt;// run inference in a worker&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;segment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;background-removal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;onnx-community/ormbg-ONNX&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;dtype&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;q8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;]&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;segment&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RawImage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromCanvas&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The quantised model is about 45 MB. It downloads the first time someone opens the tool and is cached after that. A photo takes around 10 seconds on a normal laptop CPU. For logos and signatures on a flat colour there's also a non-AI mode that removes the colour directly, including the inside of letters.&lt;/p&gt;

&lt;h2&gt;
  
  
  PDF and Word without libraries
&lt;/h2&gt;

&lt;p&gt;The PDF tools write PDF 1.4 by hand: the standard 14 fonts with real font metrics for Latin text, JPEG images embedded as they are, and Flate-compressed content streams. Text in scripts the standard fonts can't show, such as Bangla, is drawn by the browser and embedded as a lossless image, so it always looks right.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;.docx&lt;/code&gt; files are ZIP archives of XML, so Word to PDF unzips them with &lt;code&gt;DecompressionStream&lt;/code&gt;, reads &lt;code&gt;word/document.xml&lt;/code&gt; with &lt;code&gt;DOMParser&lt;/code&gt; and lays out the paragraphs, lists and tables. The Text to PDF editor goes the other way and exports both PDF and a real &lt;code&gt;.docx&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd like feedback on
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Target size:&lt;/strong&gt; is the "highest quality that fits" result good enough, or should it trade some resolution for quality sooner?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Background removal:&lt;/strong&gt; are 10 seconds and a one-time 45 MB download acceptable for you, or would you rather have a smaller, faster model with rougher edges?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can try everything here, with no account: &lt;strong&gt;&lt;a href="https://compresslyonline.blogspot.com/" rel="noopener noreferrer"&gt;compresslyonline.blogspot.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://compresslyonline.blogspot.com/p/compress-image.html" rel="noopener noreferrer"&gt;Compress an image to 100 KB&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://compresslyonline.blogspot.com/p/remove-background.html" rel="noopener noreferrer"&gt;Remove a background&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://compresslyonline.blogspot.com/p/image-to-pdf.html" rel="noopener noreferrer"&gt;Convert images to PDF&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

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