<?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: Fahad Farid</title>
    <description>The latest articles on DEV Community by Fahad Farid (@toolvero).</description>
    <link>https://dev.to/toolvero</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%2F4183099%2F6e6e3bf5-2e6d-4efc-a4c9-134f45968eb3.png</url>
      <title>DEV Community: Fahad Farid</title>
      <link>https://dev.to/toolvero</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/toolvero"/>
    <language>en</language>
    <item>
      <title>How we audit local PDF processing (and what "no upload" actually proves)</title>
      <dc:creator>Fahad Farid</dc:creator>
      <pubDate>Sun, 18 Oct 2026 19:45:22 +0000</pubDate>
      <link>https://dev.to/toolvero/how-we-audit-local-pdf-processing-and-what-no-upload-actually-proves-1o1n</link>
      <guid>https://dev.to/toolvero/how-we-audit-local-pdf-processing-and-what-no-upload-actually-proves-1o1n</guid>
      <description>&lt;p&gt;Tags: javascript, webdev, privacy, tutorial&lt;/p&gt;




&lt;p&gt;"No upload" is a claim. Anyone can write it under a button. So here's how we actually check it on ToolVero — and, just as important, what the check can't prove.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: grep the source for upload mechanisms
&lt;/h2&gt;

&lt;p&gt;The blunt version of the audit is a static source review. We took all 74 public tool pages, their inline scripts, and the 8 first-party scripts they reference, and searched for the four ways a browser page can send data out: &lt;code&gt;fetch()&lt;/code&gt;, &lt;code&gt;XMLHttpRequest&lt;/code&gt;, &lt;code&gt;WebSocket&lt;/code&gt;, and &lt;code&gt;sendBeacon&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Result: no direct call to a ToolVero document-processing upload endpoint anywhere in tool code. That's the finding behind the "Private browser processing" badge — 74 pages checked, 42 of them file tools, zero first-party upload paths.&lt;/p&gt;

&lt;p&gt;But a static review has a defined edge, and we publish it: it doesn't inspect third-party library internals, it doesn't observe runtime network traffic, and it doesn't mean the pages make no network requests at all. A page can load a library from a CDN, fire analytics, and serve ads — none of which is your file going to our server, but all of which is network activity. The badge wording says exactly this: the &lt;em&gt;file&lt;/em&gt; isn't sent to a ToolVero processing server &lt;em&gt;by the tool itself&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: design the exfiltration out, don't promise it away
&lt;/h2&gt;

&lt;p&gt;The stronger audit pattern is clean-by-construction. Our &lt;a href="https://toolvero.co/tools/image-metadata-remover/" rel="noopener noreferrer"&gt;Image Metadata Remover&lt;/a&gt; doesn't promise "we won't keep your EXIF data" — it makes keeping it structurally impossible. The pipeline decodes the image with &lt;code&gt;createImageBitmap&lt;/code&gt;, rasterises it to a canvas, and rebuilds a fresh PNG that retains only the signature plus &lt;code&gt;IHDR&lt;/code&gt;, &lt;code&gt;IDAT&lt;/code&gt; and &lt;code&gt;IEND&lt;/code&gt; chunks. There is no chunk in the output where EXIF, GPS, XMP or thumbnails could hide, because the output is assembled from pixels, not copied from the source container.&lt;/p&gt;

&lt;p&gt;When the design removes the possibility, the audit only has to verify the design — a much smaller claim to defend than "trust our servers."&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: publish what the audit finds, including the fixes
&lt;/h2&gt;

&lt;p&gt;An audit that never finds anything isn't an audit, it's marketing. Ours found a real one: the Word-to-PDF converter was inserting HTML returned from a DOCX conversion library straight into the page, and that library's own docs warn its output isn't sanitized. We fixed it by rebuilding only a limited set of text, table and image elements — no script elements, no event handlers, no styles, no unsafe URL attributes carried through. Some advanced DOCX formatting gets dropped as a result, and we say so on the page.&lt;/p&gt;

&lt;p&gt;The audit page also lists remaining work in plain text: at least 19 file-tool pages still load externally hosted library scripts, analytics and advertising scripts are present, and there's been no independent penetration test or live network capture with sample files. That's the current boundary of what we know.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: keep the categories separate
&lt;/h2&gt;

&lt;p&gt;This is the part most "privacy" pages fudge. On our &lt;a href="https://toolvero.co/trust/" rel="noopener noreferrer"&gt;Trust Center&lt;/a&gt; we keep four things in separate boxes: the privacy &lt;em&gt;design&lt;/em&gt; (browser-first processing), the &lt;em&gt;technical audit&lt;/em&gt; (the source review above), &lt;em&gt;third-party scans&lt;/em&gt; (SSL Labs, SecurityHeaders, Sucuri — transport and header checks, dated, with their own limits), and &lt;em&gt;formal certification&lt;/em&gt; (which we don't claim to hold). A scan result is not a certification. An internal review is not a penetration test. Saying which is which is the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway for builders
&lt;/h2&gt;

&lt;p&gt;If you ship browser-local tools, audit like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Grep for exfiltration primitives&lt;/strong&gt; (&lt;code&gt;fetch&lt;/code&gt;, XHR, WebSocket, &lt;code&gt;sendBeacon&lt;/code&gt;) across every page and first-party script. It's crude and it catches the obvious.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefer clean-by-construction pipelines&lt;/strong&gt; — rebuild outputs from primitives so the sensitive data can't survive even if you wanted it to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publish the scope box&lt;/strong&gt;: what you checked, what you explicitly didn't, and what's still open. The &lt;a href="https://toolvero.co/privacy-center/" rel="noopener noreferrer"&gt;full audit&lt;/a&gt; is dated October 10, 2026, and it reads like a lab notebook, not a badge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never let marketing outrun the audit.&lt;/strong&gt; Our commitment is mechanical: if a tool ever moves from local to server-side processing, its privacy explanation changes with it — no stale claims.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;"No upload" is only as strong as the audit behind it. Ours is public, dated, and honest about its edges — which is exactly why we'd rather you read it than trust it.&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>privacy</category>
      <category>security</category>
    </item>
    <item>
      <title>Merging PDFs in the browser: no server, no upload</title>
      <dc:creator>Fahad Farid</dc:creator>
      <pubDate>Sat, 17 Oct 2026 15:23:56 +0000</pubDate>
      <link>https://dev.to/toolvero/merging-pdfs-in-the-browser-no-server-no-upload-306a</link>
      <guid>https://dev.to/toolvero/merging-pdfs-in-the-browser-no-server-no-upload-306a</guid>
      <description>&lt;p&gt;Merging PDFs sounds like server work, but the PDF format is just a structured binary file — and libraries like pdf-lib can read and write it entirely in JavaScript. So the merge can happen in the tab, with your documents never uploaded anywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole trick
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PDFDocument&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pdf-lib&lt;/span&gt;&lt;span class="dl"&gt;'&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;mergePDFs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;files&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;merged&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;PDFDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&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;const&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;files&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;pdf&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;PDFDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&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;pages&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;merged&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copyPages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPageIndices&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="nx"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;merged&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&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;bytes&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;merged&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/pdf&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's genuinely it: load each file into memory, copy its pages into a fresh document, save. &lt;code&gt;copyPages&lt;/code&gt; preserves vector content, fonts, and images — the output isn't a rasterized screenshot, it's a real PDF.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gotchas
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Encrypted PDFs:&lt;/strong&gt; pdf-lib can't open password-protected files silently. Pass the password via the &lt;code&gt;load&lt;/code&gt; options, or handle the failure gracefully and tell the user why.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory:&lt;/strong&gt; everything lives in RAM. Fine for typical documents; on mobile, warn before merging hundred-page scans.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forms and annotations&lt;/strong&gt; come along with &lt;code&gt;copyPages&lt;/code&gt;. If you need a flat, uneditable result, flatten form fields before saving.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Page sizes:&lt;/strong&gt; mixed source sizes are preserved per page, which is usually what you want — don't "normalize" unless the user asks.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why client-side wins here
&lt;/h2&gt;

&lt;p&gt;PDFs are often sensitive — contracts, invoices, ID scans. The privacy argument writes itself: no upload means no transit interception, no server retention policy to trust, and it works offline once the page loads.&lt;/p&gt;

&lt;p&gt;The same approach powers the &lt;a href="https://toolvero.co/tools/merge-pdf/" rel="noopener noreferrer"&gt;Merge PDF&lt;/a&gt; and &lt;a href="https://toolvero.co/tools/compress-pdf/" rel="noopener noreferrer"&gt;Compress PDF&lt;/a&gt; tools on &lt;a href="https://toolvero.co" rel="noopener noreferrer"&gt;ToolVero&lt;/a&gt; — free, no signup, files never leave your browser.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>pdf</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Your password generator is probably using Math.random(). Stop.</title>
      <dc:creator>Fahad Farid</dc:creator>
      <pubDate>Thu, 15 Oct 2026 08:55:47 +0000</pubDate>
      <link>https://dev.to/toolvero/your-password-generator-is-probably-using-mathrandom-stop-5eab</link>
      <guid>https://dev.to/toolvero/your-password-generator-is-probably-using-mathrandom-stop-5eab</guid>
      <description>&lt;p&gt;&lt;code&gt;Math.random()&lt;/code&gt; is not cryptographically secure. Its output is deterministic given the internal state, which means passwords generated with it are guessable in principle. Any password or token generator must use the Web Crypto API instead: &lt;code&gt;crypto.getRandomValues()&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The correct core
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;securePassword&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&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;charset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#$%^&amp;amp;*&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="nx"&gt;bytes&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;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;length&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="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRandomValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// CSPRNG, provided by the OS&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;charset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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;out&lt;/span&gt; &lt;span class="o"&gt;=&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;const&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Rejection sampling: skip bytes that would cause modulo bias&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;b&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;charset&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;charset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details worth understanding:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;crypto.getRandomValues()&lt;/code&gt;&lt;/strong&gt; pulls from the operating system's cryptographically secure random number generator — the same source TLS uses. It cannot be seeded or predicted from JavaScript.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rejection sampling&lt;/strong&gt; fixes modulo bias. If your charset length doesn't divide 256 evenly, a naive &lt;code&gt;byte % charset.length&lt;/code&gt; makes some characters slightly more likely. Discarding out-of-range bytes keeps the distribution uniform. (In production, loop and refill until the password is full rather than hoping one buffer is enough.)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How strong is it?
&lt;/h2&gt;

&lt;p&gt;With a 70-character set and 20 characters, you get log2(70^20) ≈ 122 bits of entropy. That's beyond brute force for any realistic adversary — length plus a real CSPRNG is the entire game.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Never &lt;code&gt;Math.random()&lt;/code&gt; for secrets. Ever.&lt;/li&gt;
&lt;li&gt;Generate locally — a password that never touches a network can't be intercepted in transit or logged by a server.&lt;/li&gt;
&lt;li&gt;Don't get clever with "memorable" patterns that shrink the search space.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Want one without writing it yourself? The &lt;a href="https://toolvero.co/tools/password-generator/" rel="noopener noreferrer"&gt;Password Generator&lt;/a&gt; on &lt;a href="https://toolvero.co" rel="noopener noreferrer"&gt;ToolVero&lt;/a&gt; uses exactly this approach — everything runs locally in your tab.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How I shrink images 80% without a server</title>
      <dc:creator>Fahad Farid</dc:creator>
      <pubDate>Wed, 14 Oct 2026 12:21:24 +0000</pubDate>
      <link>https://dev.to/toolvero/how-i-shrink-images-80-without-a-server-2jgk</link>
      <guid>https://dev.to/toolvero/how-i-shrink-images-80-without-a-server-2jgk</guid>
      <description>&lt;p&gt;Every image optimizer site asks you to upload your photos first. But your browser can already decode, resize, and re-encode images — so the whole compression pipeline can run in the tab, with the original file never leaving your device.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline
&lt;/h2&gt;

&lt;p&gt;Three steps, all client-side:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Decode the uploaded file into pixels (&lt;code&gt;createImageBitmap&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Draw it onto a canvas at the target dimensions — this is where resizing happens, and it's the biggest win.&lt;/li&gt;
&lt;li&gt;Export with &lt;code&gt;canvas.toBlob()&lt;/code&gt; at a chosen quality and format.
&lt;/li&gt;
&lt;/ol&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;compress&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;maxWidth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;quality&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.8&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;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="mi"&gt;1&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;bitmap&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="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="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;bitmap&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="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="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;bitmap&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;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="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;canvas&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;canvas&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="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="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/webp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;)&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;h2&gt;
  
  
  Why it works so well
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Resizing dominates.&lt;/strong&gt; A 6000px phone photo displayed at 1200px carries roughly 25x the pixels it needs. Nothing beats deleting pixels you were never going to show.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Modern formats.&lt;/strong&gt; WebP at quality 0.75–0.85 is visually near-identical to the source JPEG for photographs, at a fraction of the size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metadata goes free.&lt;/strong&gt; The decode/re-encode round-trip drops EXIF segments automatically — the output is pixels only.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The gotchas
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Screenshots and flat graphics:&lt;/strong&gt; lossy compression smears text and sharp edges. Use WebP lossless or PNG for those.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transparency:&lt;/strong&gt; JPEG has no alpha channel. If the source has transparency, export WebP or PNG instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Orientation:&lt;/strong&gt; modern browsers apply EXIF orientation when decoding via &lt;code&gt;createImageBitmap&lt;/code&gt;, but verify on Safari before you ship.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No upload means no waiting on a server round-trip either — on a decent laptop this runs in milliseconds per image.&lt;/p&gt;

&lt;p&gt;I build &lt;a href="https://toolvero.co" rel="noopener noreferrer"&gt;ToolVero&lt;/a&gt;, 70+ free tools that run entirely in your browser. The same pipeline powers the &lt;a href="https://toolvero.co/tools/image-compressor/" rel="noopener noreferrer"&gt;Image Compressor&lt;/a&gt; and &lt;a href="https://toolvero.co/tools/image-resizer/" rel="noopener noreferrer"&gt;Image Resizer&lt;/a&gt; — try them on your own photos.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>performance</category>
    </item>
    <item>
      <title>How I strip photo metadata without a server</title>
      <dc:creator>Fahad Farid</dc:creator>
      <pubDate>Tue, 13 Oct 2026 20:58:02 +0000</pubDate>
      <link>https://dev.to/toolvero/how-i-strip-photo-metadata-without-a-server-dn0</link>
      <guid>https://dev.to/toolvero/how-i-strip-photo-metadata-without-a-server-dn0</guid>
      <description>&lt;p&gt;Your phone embeds a small diary in every photo: GPS coordinates, camera model, timestamps. Most “metadata remover” sites ask you to upload that diary to &lt;em&gt;their&lt;/em&gt; server first — which rather defeats the purpose.&lt;/p&gt;

&lt;p&gt;I build &lt;a href="https://toolvero.co" rel="noopener noreferrer"&gt;ToolVero&lt;/a&gt;, a set of 70+ free tools that run entirely in your browser, and the image metadata remover takes the opposite approach: the photo never leaves the device. Here’s the whole trick.&lt;/p&gt;

&lt;h3&gt;
  
  
  The method: decode, then re-encode from pixels
&lt;/h3&gt;

&lt;p&gt;EXIF, GPS and XMP all live in the JPEG’s metadata segments — they are not part of the image data itself. So instead of surgically deleting segments, we throw the container away:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Decode the uploaded JPEG into a canvas bitmap (pixels only — the browser’s image decoder does not carry EXIF into the bitmap).&lt;/li&gt;
&lt;li&gt;Create a fresh PNG from those pixels with &lt;code&gt;canvas.toBlob('image/png')&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Offer that PNG as the download, under a neutral filename.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A PNG produced this way contains only IHDR, IDAT and IEND chunks. There is simply nowhere for EXIF to hide, because we never copied the original file’s segments — we re-created the image from raw pixels.&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;img&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="c1"&gt;// pixels only, no EXIF&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;img&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;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;img&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="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="nf"&gt;drawImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;img&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clean&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;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;r&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;r&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/png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="c1"&gt;// `clean` is a fresh PNG: IHDR + IDAT + IEND, zero metadata segments&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why the output is clean by construction
&lt;/h3&gt;

&lt;p&gt;PNG has no EXIF segment to preserve, and this method never copies the original file’s metadata segments at all — only the pixels. So EXIF, GPS and XMP have nowhere to survive. (Re-encoding to JPEG via canvas is equally clean, since canvas never writes EXIF — but PNG is the safer default because the format itself can’t carry the metadata.)&lt;/p&gt;

&lt;h3&gt;
  
  
  Why client-side matters
&lt;/h3&gt;

&lt;p&gt;The privacy win isn’t just the stripped file — it’s that the stripping happens on your machine. No upload means no transit interception, no server logs, no “we delete after 24 hours” promises to trust. For a privacy tool, the architecture &lt;em&gt;is&lt;/em&gt; the feature.&lt;/p&gt;

&lt;p&gt;Try it on your own photos: &lt;a href="https://toolvero.co/tools/image-metadata-remover/" rel="noopener noreferrer"&gt;Image Metadata Remover&lt;/a&gt; — everything runs locally in the tab.&lt;/p&gt;

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