<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Toolsy Hub]]></title><description><![CDATA[Toolsy Hub]]></description><link>https://toolsyhub.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6ada1c41172e6142e5ac6416/79277ed1-5d97-48c4-99bb-0728fc97fafe.png</url><title>Toolsy Hub</title><link>https://toolsyhub.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Sun, 01 Nov 2026 01:17:13 GMT</lastBuildDate><atom:link href="https://toolsyhub.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How to compress an image to an exact KB limit in the browser (canvas, binary search, and when to stop lowering quality)]]></title><description><![CDATA[Upload forms for visas, exams and job portals often say "photo must be under 100 KB". A phone photo is 3 to 5 MB, and the usual advice is to drag a quality…]]></description><link>https://toolsyhub.hashnode.dev/how-to-compress-an-image-to-an-exact-kb-limit-in-the-browser-canvas-binary-search-and-when-to-stop-lowering-quality</link><guid isPermaLink="true">https://toolsyhub.hashnode.dev/how-to-compress-an-image-to-an-exact-kb-limit-in-the-browser-canvas-binary-search-and-when-to-stop-lowering-quality</guid><category><![CDATA[performance]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[webdev]]></category><category><![CDATA[Tutorial]]></category><dc:creator><![CDATA[Toolsy Hub]]></dc:creator><pubDate>Thu, 29 Oct 2026 02:56:11 GMT</pubDate><content:encoded><![CDATA[<p>Upload forms for visas, exams and job portals often say "photo must be under 100 KB". A phone photo is 3 to 5 MB, and the usual advice is to drag a quality slider until the number looks right. You can automate that in the browser with the canvas API, and the part that matters is knowing when to stop lowering quality.</p>
<p><strong>Encode at a quality and read the size</strong></p>
<p><code>canvas.toBlob(callback, "image/jpeg", quality)</code> gives you a Blob. <code>blob.size</code> is the file size in bytes. That is all you need to measure.</p>
<pre><code class="language-js">function encode(bitmap, width, height, quality) {
  const canvas = document.createElement("canvas")
  canvas.width = width
  canvas.height = height
  const ctx = canvas.getContext("2d")
  ctx.fillStyle = "#fff" // JPEG has no alpha; transparent PNGs turn black without this
  ctx.fillRect(0, 0, width, height)
  ctx.imageSmoothingQuality = "high"
  ctx.drawImage(bitmap, 0, 0, width, height)
  return new Promise((resolve) =&gt; canvas.toBlob(resolve, "image/jpeg", quality))
}
</code></pre>
<p>Load the bitmap with <code>createImageBitmap(file, { imageOrientation: "from-image" })</code>, otherwise phone photos come out sideways because the EXIF rotation is ignored.</p>
<p><strong>Binary search on quality</strong></p>
<p>File size grows with quality, so you can search for the highest quality that still fits:</p>
<pre><code class="language-js">let low = 0.3, high = 0.95, best = null
for (let i = 0; i &lt; 6; i++) {
  const q = (low + high) / 2
  const blob = await encode(bitmap, w, h, q)
  if (blob.size &lt;= targetBytes) { best = blob; low = q } else { high = q }
}
</code></pre>
<p>Six rounds narrows the range to about 1% of quality, which is more precision than the file size needs. Count 1 KB as 1000 bytes, not 1024, so the file passes whether the form counts KB one way or the other.</p>
<p><strong>Know when to stop</strong></p>
<p>Here is the part the sliders do not tell you. Below roughly 30% JPEG quality a photo breaks into visible blocks and the colours shift. A file that is technically under the limit but looks like that gets rejected by a human reviewer or looks bad on an ID. So the search never goes below 0.3. First it encodes once at 0.3 for the current size. If that is still over the limit, it makes the picture smaller instead: width and height times 0.8, and try again. It tries up to 12 sizes, which is down to about a tenth of the original width. Only at the last size does it allow quality all the way down to 0.05, because a file that fits beats no file.</p>
<p>PNG has no quality setting, so for PNG only the dimensions change.</p>
<p><strong>What I would do differently</strong></p>
<ul>
<li>Offer a "keep exact width and height" switch for forms that demand fixed pixels, and then accept a lower quality or a missed target. The tool I built has one, and it tells the user when the target was missed.</li>
<li>Test the search with a fake encoder. The function takes the encoder as a parameter, so unit tests can pass a function that returns a Blob of a size you choose, with no canvas involved.</li>
</ul>
<p>The tool built on this, with nothing uploaded anywhere:</p>
<ul>
<li>Image compressor with a target size: <a href="https://www.toolsy-hub.com/tools/image-compressor">https://www.toolsy-hub.com/tools/image-compressor</a></li>
<li>Photo resizer for exact KB and pixel presets: <a href="https://www.toolsy-hub.com/tools/photo-signature-resizer">https://www.toolsy-hub.com/tools/photo-signature-resizer</a></li>
</ul>
<p>Google Analytics counts visits on the site; image files are never sent anywhere.</p>
]]></content:encoded></item><item><title><![CDATA[Building PDF tools that never upload your file: pdf-lib, pdf.js and qpdf in WebAssembly]]></title><description><![CDATA[How merge, split, compress and unlock PDF run fully in the browser with pdf-lib, pdf.js and qpdf compiled to WebAssembly, and where that falls short.]]></description><link>https://toolsyhub.hashnode.dev/building-pdf-tools-that-never-upload-your-file-pdf-lib-pdf-js-and-qpdf-in-webassembly</link><guid isPermaLink="true">https://toolsyhub.hashnode.dev/building-pdf-tools-that-never-upload-your-file-pdf-lib-pdf-js-and-qpdf-in-webassembly</guid><category><![CDATA[pdf]]></category><category><![CDATA[WebAssembly]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[webdev]]></category><dc:creator><![CDATA[Toolsy Hub]]></dc:creator><pubDate>Thu, 22 Oct 2026 14:28:05 GMT</pubDate><content:encoded><![CDATA[<p>Most "free PDF tools" upload your file to a server, process it there and send it back. For a bank statement or a CNIC scan that is a lot of trust to hand a site you found a minute ago. I wanted merge, split, compress and unlock to work with no upload at all, so I built them to run entirely in the browser. This post is what that took, and where it falls short.</p>
<h2>The three engines</h2>
<ul>
<li><strong>pdf-lib</strong> (1.17) reads and writes the PDF structure: merging, splitting, picking pages, building a PDF from JPGs. It is plain JavaScript and works the same in every modern browser.</li>
<li><strong>pdf.js</strong> (Mozilla's renderer) draws a page onto a canvas. I use it to turn pages into images for "PDF to JPG" and for the stronger compress mode.</li>
<li><strong>qpdf</strong>, compiled to WebAssembly (about 1.3 MB), removes the password and restrictions from a PDF when the user types the password they already know. pdf-lib cannot decrypt, so this is the one job that needed a native tool.</li>
</ul>
<p>None of them load with the page. Each is imported inside the click handler with <code>await import(...)</code>, so someone who only wants the word counter never downloads a PDF engine.</p>
<h2>Caching the engines for a year</h2>
<p>The WASM and worker files are copied at build time into a folder named after the package version, for example <code>/engines/qpdf-&lt;version&gt;/</code>, and served with <code>Cache-Control: public, max-age=31536000, immutable</code>. A new version gets a new folder, so a browser never mixes an old worker with new code, and a returning visitor does not download 1-2 MB again.</p>
<h2>Compress: two modes, and why</h2>
<p>There is no magic "compress PDF" call. What shrinks a PDF depends on what is inside it.</p>
<ol>
<li><strong>Light mode</strong> re-saves the file with pdf-lib using compressed object streams. Text stays selectable. The saving is small, often a few percent, because the pictures inside are untouched.</li>
<li><strong>Image mode</strong> renders every page with pdf.js to a JPEG (150 dpi at quality 0.8 for "high", down to 96 dpi at 0.5 for "low") and builds a new PDF from those images. For scanned documents, which are pictures anyway, this is where the big savings are. The cost: text becomes part of the picture and is no longer selectable.</li>
</ol>
<p>The tool says which mode does what before you click, rather than after.</p>
<h2>Hitting an exact size</h2>
<p>Upload forms in Pakistan ask for "PDF under 200 KB" or "under 100 KB", so a slider is not enough. With a target set, the pages are rendered at the sharpest level first, and the packer lowers JPEG quality only as far as it has to until the whole PDF fits. If it cannot get there, it says so instead of quietly handing back a file that is still too big.</p>
<h2>What does not work well</h2>
<ul>
<li><strong>Very large files on phones.</strong> Rendering 60 pages at 150 dpi in a phone browser uses a lot of memory. Server tools win here.</li>
<li><strong>Image mode on text PDFs.</strong> A text-only PDF can come out bigger as images. Light mode is the right choice there. Image mode is still the default, because most files people bring to a "PDF under 200 KB" form are scans.</li>
<li><strong>Password cracking.</strong> The unlock tool opens a file with the password you type. It does not guess passwords, on purpose.</li>
</ul>
<h2>Try it</h2>
<p>The tools are free with no sign-up: <a href="https://www.toolsy-hub.com/tools/compress-pdf">Compress PDF</a>, <a href="https://www.toolsy-hub.com/tools/merge-pdf">Merge PDF</a>, <a href="https://www.toolsy-hub.com/tools/unlock-pdf">Unlock PDF</a>. Open the network tab while you use one and you will see no request carrying your file.</p>
<p>If you have built something similar, I would like to hear how you dealt with memory on phones.</p>
]]></content:encoded></item></channel></rss>