<?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>Forem: Shakeel Skl</title>
    <description>The latest articles on Forem by Shakeel Skl (@shakeel_skl_019683a0a1836).</description>
    <link>https://forem.com/shakeel_skl_019683a0a1836</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3839873%2Fe7a5c4c8-1481-4c37-872b-143b2ddcf063.png</url>
      <title>Forem: Shakeel Skl</title>
      <link>https://forem.com/shakeel_skl_019683a0a1836</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/shakeel_skl_019683a0a1836"/>
    <language>en</language>
    <item>
      <title>Stop pasting API keys into online formatters and croppers</title>
      <dc:creator>Shakeel Skl</dc:creator>
      <pubDate>Tue, 05 May 2026 19:41:27 +0000</pubDate>
      <link>https://forem.com/shakeel_skl_019683a0a1836/stop-pasting-api-keys-into-online-formatters-and-croppers-3n9b</link>
      <guid>https://forem.com/shakeel_skl_019683a0a1836/stop-pasting-api-keys-into-online-formatters-and-croppers-3n9b</guid>
      <description>&lt;p&gt;Every day, millions of developers copy-paste sensitive data into random online tools.&lt;/p&gt;

&lt;p&gt;A JSON response from a production server into an online formatter. A snippet containing a database connection string into a snippet manager. A UI design file from Figma into an online image cropper.&lt;/p&gt;

&lt;p&gt;We do it because it’s fast. But we rarely stop to ask: &lt;em&gt;where is this data actually going?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you paste a JSON payload with a live Bearer token into a standard online formatter, that token travels across the internet to a third-party server. If you upload a design file containing proprietary UI to an online cropper, that intellectual property is now sitting on someone else's hard drive.&lt;/p&gt;

&lt;p&gt;For side projects, we might not care. But for client work or proprietary code, this is a massive security hole.&lt;/p&gt;

&lt;h2&gt;The "Local-Only" Alternative&lt;/h2&gt;

&lt;p&gt;Over the last few months, I've been building a suite of browser-native tools that process everything locally. Because they use native JavaScript APIs, the data never triggers a network request.&lt;/p&gt;

&lt;p&gt;If you open your browser's DevTools (F12 -&amp;gt; Network tab) and use these tools, you will see exactly zero outbound HTTP requests.&lt;/p&gt;

&lt;p&gt;Here are the three I use the most:&lt;/p&gt;

&lt;h3&gt;1. A Private JSON Formatter&lt;/h3&gt;

&lt;p&gt;When you need to beautify or validate a JSON response, you shouldn't have to send your API keys to a server. I use a &lt;a href="https://solvebar.com/tools/json-formatter" rel="noopener noreferrer"&gt;private JSON formatter&lt;/a&gt; that runs entirely in the browser tab. It handles deeply nested objects and catches trailing comma errors without uploading the payload.&lt;/p&gt;

&lt;h3&gt;2. A Code Snippet Manager&lt;/h3&gt;

&lt;p&gt;Storing code snippets in Notion or GitHub Gists is fine for public stuff, but what about private client logic? I use a &lt;a href="https://solvebar.com/tools/code-snippet-manager" rel="noopener noreferrer"&gt;browser-based snippet manager&lt;/a&gt; that saves everything to &lt;code&gt;localStorage&lt;/code&gt;. It supports syntax highlighting for 20+ languages, requires zero login, and keeps your proprietary logic completely off the cloud.&lt;/p&gt;

&lt;h3&gt;3. A "Fig Cropper" and Image Tool&lt;/h3&gt;

&lt;p&gt;Designers often export UI frames from Figma and hand them off to developers. If you need to crop or resize them, uploading them to a random server is a privacy risk. I use an &lt;a href="https://solvebar.com/tools/image-cropper" rel="noopener noreferrer"&gt;image cropper&lt;/a&gt; that works as a Fig cropper—handling high-res design files directly in the browser. It also includes a built-in &lt;a href="https://solvebar.com/tools/image-converter" rel="noopener noreferrer"&gt;image converter&lt;/a&gt; to switch between JPG, PNG, and WebP without a server upload.&lt;/p&gt;

&lt;h2&gt;Why this matters for client work&lt;/h2&gt;

&lt;p&gt;If you are handling files for a healthcare client, a fintech startup, or a legally binding contract, sending that data to a random SaaS tool might actually violate your NDA or compliance requirements (like HIPAA or GDPR).&lt;/p&gt;

&lt;p&gt;Using local-only tools isn't just a "nice-to-have" privacy flex. It's a professional requirement.&lt;/p&gt;

&lt;p&gt;Next time you need to format, crop, or store something sensitive, check your Network tab. If you see data leaving your browser, close the tab and find a local alternative.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>private</category>
    </item>
    <item>
      <title>browser-native crypto utilities every Web3 dev needs</title>
      <dc:creator>Shakeel Skl</dc:creator>
      <pubDate>Sat, 18 Apr 2026 14:47:30 +0000</pubDate>
      <link>https://forem.com/shakeel_skl_019683a0a1836/browser-native-crypto-utilities-every-web3-dev-needs-lk2</link>
      <guid>https://forem.com/shakeel_skl_019683a0a1836/browser-native-crypto-utilities-every-web3-dev-needs-lk2</guid>
      <description>&lt;p&gt;Here is the complete material for your second dev.to article, targeting the crypto programmatic tools we built. &lt;/p&gt;

&lt;p&gt;Again, this is pure Markdown with angle brackets &lt;code&gt;&amp;lt; &amp;gt;&lt;/code&gt; around the URLs so dev.to doesn't break the links. I also included the strict single-word tags at the bottom.&lt;/p&gt;

&lt;p&gt;As a web dev, I've lost count of how many times a crypto project I was building needed a quick unit conversion or a data fetch. &lt;/p&gt;

&lt;p&gt;When you're working with Web3.js, Ethers, or just building a simple dashboard, you don't want to import a massive 5MB library just to convert SOL to USDC or fetch a token's current price. &lt;/p&gt;

&lt;p&gt;To speed up my own workflow, I built a suite of zero-dependency, browser-native crypto utilities. No API keys required, no JWT tokens, just instant client-side lookups.&lt;/p&gt;

&lt;p&gt;Here are the three I use the most:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Instant Token Price Lookups
&lt;/h3&gt;

&lt;p&gt;Need to display the live price of ETH, BTC, or an altcoin in your React state? I got tired of reading CoinGecko documentation, so I made a &lt;a href="https://solvebar.com/tools/crypto-watchlist" rel="noopener noreferrer"&gt;crypto price checker&lt;/a&gt; where you just type the ticker (e.g., "PEPE"). It fetches the price, market cap, and 24h change instantly. &lt;/p&gt;

&lt;h3&gt;
  
  
  2. The Unit Converter We Actually Need
&lt;/h3&gt;

&lt;p&gt;If you've ever tried to manually calculate how many Gwei equals 1 ETH, or how many Lamports are in a 5 SOL transaction, you know the math is annoying. I built a &lt;a href="https://solvebar.com/tools/eth-gas-tracker" rel="noopener noreferrer"&gt;crypto unit converter&lt;/a&gt; that handles the specific denominations for Bitcoin (BTC/Sats), Ethereum (ETH/Gwei/Wei), and Solana (SOL/Lamports).&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Gas Estimator for Quick Math
&lt;/h3&gt;

&lt;p&gt;Before sending a transaction on an EVM chain, I usually want a rough estimate of the fiat cost. Instead of switching tabs to a block explorer, I use this &lt;a href="https://solvebar.com/tools/eth-gas-tracker" rel="noopener noreferrer"&gt;crypto gas fee estimator&lt;/a&gt;. You plug in the gas limit and Gwei, and it tells you exactly what you'll pay in USD.&lt;/p&gt;

&lt;p&gt;All of these just wrap standard REST APIs but format the output specifically for developers. No fluff, no ads, just the data. &lt;/p&gt;

&lt;p&gt;What's your most annoying repetitive task when building crypto dashboards?&lt;/p&gt;

</description>
      <category>web3</category>
      <category>javascript</category>
      <category>cryptocurrency</category>
      <category>private</category>
    </item>
    <item>
      <title>How I cut my Next.js image payload by 60% without changing a line of code</title>
      <dc:creator>Shakeel Skl</dc:creator>
      <pubDate>Sat, 18 Apr 2026 14:44:30 +0000</pubDate>
      <link>https://forem.com/shakeel_skl_019683a0a1836/how-i-cut-my-nextjs-image-payload-by-60-without-changing-a-line-of-code-2mhd</link>
      <guid>https://forem.com/shakeel_skl_019683a0a1836/how-i-cut-my-nextjs-image-payload-by-60-without-changing-a-line-of-code-2mhd</guid>
      <description>&lt;p&gt;I was doing a routine Lighthouse audit on a Next.js app I'm building and realized my LCP (Largest Contentful Paint) was terrible. The culprit? Unoptimized hero images and Open Graph meta images.&lt;/p&gt;

&lt;p&gt;I didn't want to set up a heavy Sharp pipeline in my CI/CD just for a few static assets. I needed a quick way to optimize them before committing. &lt;/p&gt;

&lt;p&gt;Here is the exact 3-step workflow I used to fix it:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Convert formats to WebP
&lt;/h3&gt;

&lt;p&gt;Browsers love WebP. I had a few legacy PNGs that were huge. Instead of opening Photoshop, I dragged them into this &lt;a href="https://solvebar.com/tools/image-converter/png-to-webp" rel="noopener noreferrer"&gt;PNG to WebP converter&lt;/a&gt;. It runs 100% in the browser using the Canvas API, so you don't have to upload your assets to a random server.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Compress the file size
&lt;/h3&gt;

&lt;p&gt;Even some of my existing JPGs were too bloated. I ran them through this &lt;a href="https://solvebar.com/tools/image-compressor" rel="noopener noreferrer"&gt;bulk image compressor&lt;/a&gt;. You can just drag and drop 10 files at once, and it spits out optimized versions alongside a data table showing exactly how much KB you saved per file.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Fix layout shift with exact dimensions
&lt;/h3&gt;

&lt;p&gt;I was getting hit with CLS (Cumulative Layout Shift) penalties because my images didn't have explicit width and height props in my &lt;code&gt;&amp;lt;Image /&amp;gt;&lt;/code&gt; tags. I used this &lt;a href="https://solvebar.com/tools/image-resizer" rel="noopener noreferrer"&gt;image resizer&lt;/a&gt; to batch-crop all my blog headers to exactly 1200x630, eliminating the shift.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The result:&lt;/strong&gt; &lt;br&gt;
Lighthouse mobile score went from 54 to 94. No new npm packages installed. No server uploads. Just browser-native APIs. &lt;/p&gt;

&lt;p&gt;What's your go-to workflow for handling images before deploying?&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>performance</category>
      <category>imageresizer</category>
    </item>
    <item>
      <title>5 PDF Tools That Never Upload Your Files</title>
      <dc:creator>Shakeel Skl</dc:creator>
      <pubDate>Mon, 30 Mar 2026 20:38:34 +0000</pubDate>
      <link>https://forem.com/shakeel_skl_019683a0a1836/5-pdf-tools-that-never-upload-your-files-4fl6</link>
      <guid>https://forem.com/shakeel_skl_019683a0a1836/5-pdf-tools-that-never-upload-your-files-4fl6</guid>
      <description>&lt;p&gt;We've all been there. You need to merge two PDFs quickly, so you Google "merge PDF free", click the first result, upload your documents — and then realize you just sent potentially sensitive files to some random server in another country.&lt;/p&gt;

&lt;p&gt;For most people, that's fine. But what if those PDFs contain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A signed contract with client details&lt;/li&gt;
&lt;li&gt;A bank statement&lt;/li&gt;
&lt;li&gt;A confidential business proposal&lt;/li&gt;
&lt;li&gt;Medical records&lt;/li&gt;
&lt;li&gt;Legal documents&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Suddenly that "free" tool doesn't feel so free.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem With Most Free PDF Tools
&lt;/h2&gt;

&lt;p&gt;Most online PDF tools work the same way:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You upload your file to their server&lt;/li&gt;
&lt;li&gt;Their server processes it&lt;/li&gt;
&lt;li&gt;You download the result&lt;/li&gt;
&lt;li&gt;Your file sits on their server &lt;em&gt;(for how long? who knows)&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Some tools are transparent about this. Many aren't. And even the honest ones — you're trusting their security, their data retention policies, and that they won't sell or misuse your data.&lt;/p&gt;

&lt;p&gt;There's a better way.&lt;/p&gt;




&lt;h2&gt;
  
  
  Browser-Based PDF Processing — How It Works
&lt;/h2&gt;

&lt;p&gt;Modern browsers are powerful enough to process PDF files entirely locally using JavaScript libraries like &lt;strong&gt;PDF.js&lt;/strong&gt; and &lt;strong&gt;pdf-lib&lt;/strong&gt;. This means a well-built tool can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read your PDF file in memory&lt;/li&gt;
&lt;li&gt;Process it (merge, split, compress, whatever)&lt;/li&gt;
&lt;li&gt;Give you the result to download&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All without your file ever touching a server.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Your file goes: device → browser → device. That's it.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  5 PDF Tools That Work This Way
&lt;/h2&gt;

&lt;p&gt;All of these tools are available free at &lt;strong&gt;&lt;a href="https://solvebar.com" rel="noopener noreferrer"&gt;solvebar.com&lt;/a&gt;&lt;/strong&gt; — no login, no upload, no tracking.&lt;/p&gt;




&lt;ol&gt;
&lt;li&gt;PDF Merger
Combine multiple PDF files into one. Drag to reorder before merging. The entire operation runs in your browser — useful when you need to combine contracts, reports or scanned documents without exposing their contents.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When you need it: Combining a cover letter and portfolio into one file before sending a job application. Merging monthly bank statements into one annual document.&lt;/p&gt;

&lt;p&gt;👉 Merge your PDFs securely right now&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;PDF Splitter
Extract specific pages or split a PDF into individual pages. Set custom page ranges. Download split files individually or all at once as a ZIP.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When you need it: A 50-page contract where you only need to share pages 12-15 with a client. Extracting one invoice from a batch PDF.&lt;/p&gt;

&lt;p&gt;👉 Extract pages from your PDF safely&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;PDF Compressor
Reduce PDF file size by removing embedded metadata and optimizing object streams. No quality slider that secretly re-renders your document through a server.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When you need it: A scanned document that's 15MB and needs to be emailed. Reducing file size before uploading to a portal with size limits.&lt;/p&gt;

&lt;p&gt;👉 Compress your PDF without uploading it&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;PDF Watermark
Add custom text watermarks to all pages — control font, size, color, opacity and rotation. Mark documents as DRAFT, CONFIDENTIAL or SAMPLE before sharing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When you need it: Sending a proposal to a client before the contract is signed. Sharing a sample report while protecting the full version.&lt;/p&gt;

&lt;p&gt;👉 Add a watermark to your PDF locally&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;PDF Form Filler
Fill in PDF form fields directly in your browser — text fields, checkboxes, radio buttons and dropdowns. Download the completed form as PDF.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When you need it: Government forms, tax documents, application forms. These almost always contain personal information you really don't want sitting on a stranger's server.&lt;/p&gt;

&lt;p&gt;👉 Fill out your PDF forms privately&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The next time you need to process a PDF, ask yourself one question before uploading: &lt;em&gt;do I know where this file is going?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If the answer is no — or if the document contains anything sensitive — use a tool that keeps your files on your device.&lt;/p&gt;

&lt;p&gt;All five tools above are free, require no login, and process everything locally in your browser.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://solvebar.com/tools/pdf-merger" rel="noopener noreferrer"&gt;solvebar.com/tools/pdf-merger&lt;/a&gt;&lt;br&gt;&lt;br&gt;
👉 &lt;a href="https://solvebar.com/tools/pdf-splitter" rel="noopener noreferrer"&gt;solvebar.com/tools/pdf-splitter&lt;/a&gt;&lt;br&gt;&lt;br&gt;
👉 &lt;a href="https://solvebar.com/tools/pdf-compressor" rel="noopener noreferrer"&gt;solvebar.com/tools/pdf-compressor&lt;/a&gt;&lt;br&gt;&lt;br&gt;
👉 &lt;a href="https://solvebar.com/tools/pdf-watermark" rel="noopener noreferrer"&gt;solvebar.com/tools/pdf-watermark&lt;/a&gt;&lt;br&gt;&lt;br&gt;
👉 &lt;a href="https://solvebar.com/tools/pdf-form-filler" rel="noopener noreferrer"&gt;solvebar.com/tools/pdf-form-filler&lt;/a&gt;  &lt;/p&gt;




&lt;p&gt;&lt;em&gt;All tools on SolveBar run entirely in your browser. No files are uploaded, no data is tracked, no login is required.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>privacy</category>
      <category>webdev</category>
      <category>freetools</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Building browser-only PDF tools — merge, split, and canvas builder without touching a server</title>
      <dc:creator>Shakeel Skl</dc:creator>
      <pubDate>Mon, 23 Mar 2026 10:28:22 +0000</pubDate>
      <link>https://forem.com/shakeel_skl_019683a0a1836/building-browser-only-pdf-tools-merge-split-and-canvas-builder-without-touching-a-server-2517</link>
      <guid>https://forem.com/shakeel_skl_019683a0a1836/building-browser-only-pdf-tools-merge-split-and-canvas-builder-without-touching-a-server-2517</guid>
      <description>&lt;p&gt;Most PDF processing tools have a fundamental architecture problem: they upload your files to a server to do the work.&lt;/p&gt;

&lt;p&gt;This makes sense historically — PDF manipulation used to require server-side processing. But modern browser APIs have made that unnecessary for the majority of document workflows. pdf-lib, pdfjs-dist, and Fabric.js give you everything you need to merge, split, and build PDFs entirely client-side.&lt;/p&gt;

&lt;p&gt;Here's how I built three PDF tools without a single file upload.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;pdf-lib&lt;/strong&gt; — JavaScript PDF creation and modification, runs in browser and Node. Handles merging, splitting, page manipulation, metadata. The API is clean:&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="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="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="s2"&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="c1"&gt;// Merge&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;src&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="nx"&gt;file&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;indices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseRanges&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;ranges&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPageCount&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;copied&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;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;indices&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;copied&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;pdfjs-dist&lt;/strong&gt; — Mozilla's PDF renderer, runs in browser. Used for generating page thumbnails:&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;pdfjsLib&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="s2"&gt;pdfjs-dist&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;pdfjsLib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GlobalWorkerOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workerSrc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="s2"&gt;`https://cdnjs.cloudflare.com/ajax/libs/pdf.js/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;pdfjsLib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/pdf.worker.min.mjs`&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;pdfjsLib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDocument&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&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;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;promise&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;page&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;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPage&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;viewport&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getViewport&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;scale&lt;/span&gt;&lt;span class="p"&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;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="s2"&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;viewport&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;viewport&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;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;canvasContext&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="s2"&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;viewport&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;promise&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;thumb&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;toDataURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;jszip&lt;/strong&gt; — For packaging multiple split PDFs into a single ZIP download:&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;JSZip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&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="s2"&gt;jszip&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="k"&gt;default&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;zip&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;JSZip&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="nx"&gt;pageCount&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;doc&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="kd"&gt;const&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="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;doc&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;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="nx"&gt;doc&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="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;doc&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="nx"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`page-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nc"&gt;String&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;.pdf`&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;zipBlob&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;zip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateAsync&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="s2"&gt;blob&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  PDF Merger — page range selection
&lt;/h2&gt;

&lt;p&gt;The interesting part of the merger is per-file page range selection. Users can specify "1-3,5,7-9" to select specific pages from each file before merging.&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;function&lt;/span&gt; &lt;span class="nf"&gt;parseRanges&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pageCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[]&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pageCount&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&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;i&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;new&lt;/span&gt; &lt;span class="nb"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;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;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&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;part&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;)&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;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Number&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="nx"&gt;a&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="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="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pageCount&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="nx"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&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="mi"&gt;1&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="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&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;n&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;pageCount&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;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&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="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="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;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&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;a&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;b&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;With live validation and a page count display so users know exactly what will be included.&lt;/p&gt;

&lt;h2&gt;
  
  
  PDF Splitter — three modes
&lt;/h2&gt;

&lt;p&gt;Rather than one split mode, I built three:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;All pages individually&lt;/strong&gt; — splits every page into a separate PDF, packages as ZIP. Simple, covers the most common use case.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Custom ranges&lt;/strong&gt; — named ranges with labels. User creates entries like "Introduction: 1-3", "Chapter 1: 4-15", "Appendix: 16-20". Each becomes a separate PDF. Multiple ranges download as ZIP, single range downloads directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Visual page selection&lt;/strong&gt; — click thumbnails to select specific pages, downloads as single extracted PDF. Most intuitive for non-technical users.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;SplitMode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;all&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ranges&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;select&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  PDF Canvas Builder — the contextual sidebar problem
&lt;/h2&gt;

&lt;p&gt;The canvas builder uses Fabric.js. The interesting UX challenge was the property sidebar: showing all properties all the time creates visual noise and confuses users about what applies to what.&lt;/p&gt;

&lt;p&gt;The solution was a contextual sidebar that shows only properties relevant to the selected object type:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Text selected&lt;/strong&gt; → font, size, color, weight, style, alignment, line height, rotation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shape selected&lt;/strong&gt; → fill, stroke, stroke width, opacity, rotation, flip H/V&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image selected&lt;/strong&gt; → opacity, brightness, contrast, saturation, grayscale, rotation, flip H/V&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing selected&lt;/strong&gt; → hint card + add new object panels
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ObjType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;shape&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image&lt;/span&gt;&lt;span class="dl"&gt;"&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;function&lt;/span&gt; &lt;span class="nf"&gt;getObjType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;ObjType&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;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;i-text&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="s2"&gt;itext&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="s2"&gt;text&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="s2"&gt;textbox&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;includes&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="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&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;t&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image&lt;/span&gt;&lt;span class="dl"&gt;"&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;obj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;shape&lt;/span&gt;&lt;span class="dl"&gt;"&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sidebar re-renders based on &lt;code&gt;selObj.type&lt;/code&gt;, showing the right controls for whatever is selected. Image filters (brightness/contrast/saturation/grayscale) use Fabric's built-in filter pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  The SSR problem with canvas-heavy components
&lt;/h2&gt;

&lt;p&gt;Next.js SSR and Fabric.js don't mix. The solution is dynamic import with &lt;code&gt;ssr: false&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// pages/tools/pdf-builder.tsx&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PdfBuilderClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;(&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;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/components/pdf/PdfBuilderClient&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;ssr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&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;Same pattern for the merger/splitter — any component that touches &lt;code&gt;window&lt;/code&gt;, &lt;code&gt;document&lt;/code&gt;, or browser-only libraries needs this treatment in Next.js pages router.&lt;/p&gt;

&lt;h2&gt;
  
  
  Privacy by architecture
&lt;/h2&gt;

&lt;p&gt;The key insight: when you don't build a server, you can't accidentally leak data to it. The privacy guarantee isn't a policy — it's an architectural constraint.&lt;/p&gt;

&lt;p&gt;For PDF tools specifically this matters. Documents people merge and split often contain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Contracts and NDAs&lt;/li&gt;
&lt;li&gt;Financial statements&lt;/li&gt;
&lt;li&gt;Medical records&lt;/li&gt;
&lt;li&gt;Personal identification&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of those should be uploaded to a third party service just because someone needs to combine two PDFs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try them
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://solvebar.com/tools/pdf-merger" rel="noopener noreferrer"&gt;PDF Merger&lt;/a&gt;&lt;/strong&gt; — combine PDFs with page range selection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://solvebar.com/tools/pdf-splitter" rel="noopener noreferrer"&gt;PDF Splitter&lt;/a&gt;&lt;/strong&gt; — split by pages, ranges, or visual selection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://solvebar.com/tools/pdf-builder" rel="noopener noreferrer"&gt;PDF Canvas Builder&lt;/a&gt;&lt;/strong&gt; — create PDFs from scratch&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Open DevTools → Network tab while using any of them. No file upload requests. Everything stays in your browser.&lt;/p&gt;




&lt;p&gt;If you're building browser-based file processing tools, the pdf-lib + pdfjs-dist combination covers most PDF use cases without any server infrastructure. Worth knowing about.&lt;/p&gt;

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