<?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[ToolsMonk]]></title><description><![CDATA[ToolsMonk is a free online toolkit with tools covering PDF, image, text, SEO, developer, business and student tasks. Most tools run in your browser, so your fil]]></description><link>https://toolsmonk.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6ac9f1c7d3f8832d5cb57812/403be7bf-2c11-4d40-9581-d1bed6f59235.png</url><title>ToolsMonk</title><link>https://toolsmonk.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Wed, 21 Oct 2026 06:34:10 GMT</lastBuildDate><atom:link href="https://toolsmonk.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[What Actually Happens When You Generate Every App Icon Size]]></title><description><![CDATA[Why shipping app icons is harder than resizing: Apple's alpha-channel rejection, Android 66dp safe zones, real multi-resolution .ico files — solved…]]></description><link>https://toolsmonk.hashnode.dev/what-actually-happens-when-you-generate-every-app-icon-size</link><guid isPermaLink="true">https://toolsmonk.hashnode.dev/what-actually-happens-when-you-generate-every-app-icon-size</guid><category><![CDATA[Android]]></category><category><![CDATA[iOS]]></category><category><![CDATA[webdev]]></category><dc:creator><![CDATA[ToolsMonk]]></dc:creator><pubDate>Tue, 13 Oct 2026 07:11:04 GMT</pubDate><content:encoded><![CDATA[<p>"Generate all app icon sizes" sounds like a one-liner: draw the image at N resolutions, save N files. Then you read the platform requirements and find out every OS smuggles in a rule that breaks naive rendering. This post is the field guide to what a correct icon pipeline actually does — and why each step exists. (These are implemented in <a href="https://toolsmonk.com/tool/appicon">ToolsMonk's in-browser App Icon Generator</a>, but the rules apply to any pipeline, scriptable or not.)</p>
<h2>1. Downscaling is not <code>drawImage(width/2)</code> — usually</h2>
<p>Rendering a 1024×1024 source at every size from 512 down to 29 is the easy part, but quality varies with technique:</p>
<ul>
<li><p><strong>Single-step scaling</strong> from 1024 to 29px in one <code>drawImage</code> call aliases badly — fine detail collapses into moiré.</p>
</li>
<li><p><strong>Step-down halving</strong> (1024→512→256→…→32) preserves edges noticeably better because each step's kernel operates on a sane frequency range.</p>
</li>
<li><p>Browser <code>imageSmoothingQuality: "high"</code> gets you most of the way; for pixel-crisp small sizes, a sharpening pass at ≤64px recovers the bite that downscaling loses.</p>
</li>
</ul>
<h2>2. Windows: a real multi-resolution .ico</h2>
<p>The <code>.ico</code> format is a container, not an image. The binary layout:</p>
<pre><code class="language-plaintext">ICONDIR      (6 bytes)   — reserved, type=1, count
ICONDIRENTRY[n] (16 × n) — width, height, colors, planes, bpp,
                             size, offset for each embedded image
[image 0] .. [image n-1] — BMP or PNG data
</code></pre>
<p>Windows picks the <em>closest</em> entry and scales minimally, so a real <code>.ico</code> embeds 16/24/32/48/64/256px members. The classic shortcut — renaming a 256px PNG to <code>.ico</code> — technically loads, but Explorer renders it blurry at every other size, and some shell contexts (taskbar, alt-tab) misbehave. Writing the directory structure with embedded PNGs (PNG allowed for all entries since Vista) is ~50 lines of code and the difference between amateur and professional output.</p>
<h2>3. Apple: the 1024 marketing icon hates alpha</h2>
<p>The App Store <strong>rejects</strong> a 1024×1024 icon with an alpha channel. Most source artwork (and every AI-generated icon) has transparency, so a correct pipeline flattens it: composite onto an opaque background, then encode <em>without</em> alpha. The tell that a generator skipped this: your upload "looks fine in the ZIP" and App Store Connect throws <code>ERROR ITMS-90717</code> a minute later.</p>
<p>The other Apple quirk: browsers cannot emit <code>.icns</code> (it's not a web-supported container), so the honest output is an <code>AppIcon.iconset</code> folder plus the one-liner macOS already ships with:</p>
<pre><code class="language-bash">iconutil -c icns AppIcon.iconset
</code></pre>
<p>Xcode's asset catalog needs <code>Contents.json</code> — a JSON array mapping every <code>{idiom, size, scale}</code> triple to a filename. Generate it; hand-writing it is how single-scale entries go missing.</p>
<h2>4. Android: adaptive icons live on a different canvas</h2>
<p>Since API 26, the launcher icon is <strong>not an image, it's a composition</strong>: foreground and background layers on a <strong>108×108dp canvas</strong>, of which only the central <strong>66dp circle is guaranteed visible</strong>. Every OEM mask (circle, squircle, rounded square) crops differently.</p>
<p>Consequences a naive resizer ignores:</p>
<ul>
<li><p>Your logo must fit the <strong>safe zone</strong>, not the canvas edges — 20.5% of each dimension is crop territory.</p>
</li>
<li><p>Android 13's <strong>themed (monochrome) icons</strong> need a third, single-colour layer — typically the foreground's alpha silhouette.</p>
</li>
<li><p>Legacy densities (<code>mipmap-mdpi</code> … <code>xxxhdpi</code>) still ship for pre-8.0 devices.</p>
</li>
<li><p>The wiring is <code>mipmap-anydpi-v26/adaptive-icon.xml</code> referencing drawable layers.</p>
</li>
</ul>
<p>A live launcher-mask preview with a safe-zone overlay — showing how much of <em>your</em> logo each mask would crop — turns this from guesswork into a design decision.</p>
<h2>5. PWA: maskable is a purpose, not a size</h2>
<p>Web app manifests accept <code>purpose: "maskable"</code>, where the browser may crop to any shape inside a safe zone (80% of the icon per spec). Shipping only <code>"any"</code> icons is why so many PWAs show a shrunken logo floating in a white circle on Android. A correct export emits both variants and the manifest snippet to use them.</p>
<h2>6. Packaging without a server</h2>
<p>The whole pipeline — decode, scale, encode, zip — is client-side feasible: Canvas for raster ops, <code>toBlob</code>/WASM for encoding, and a pure-JS ZIP writer streaming the ~40 files into one download. Artwork never leaves the machine, which for unpublished app artwork is a real requirement, not a nice-to-have.</p>
<p>The takeaway: an icon pipeline is 10% resizing and 90% platform archaeology. Get the rules above right once, and "export icons" stops being a pre-release chore.</p>
]]></content:encoded></item><item><title><![CDATA[How a Photoshop-Style Layer Engine Fits in a Browser Tab]]></title><description><![CDATA[Canvas compositing, blend modes, paintable masks, and non-destructive adjustment layers — how a Photoshop-style editor runs entirely client-side.]]></description><link>https://toolsmonk.hashnode.dev/how-a-photoshop-style-layer-engine-fits-in-a-browser-tab</link><guid isPermaLink="true">https://toolsmonk.hashnode.dev/how-a-photoshop-style-layer-engine-fits-in-a-browser-tab</guid><category><![CDATA[JavaScript]]></category><category><![CDATA[webdev]]></category><category><![CDATA[HTML]]></category><dc:creator><![CDATA[ToolsMonk]]></dc:creator><pubDate>Tue, 13 Oct 2026 07:07:38 GMT</pubDate><content:encoded><![CDATA[<p>A layer-based photo editor sounds like desktop software. Layers, blend modes, paintable masks, non-destructive adjustments — the mental model most of us inherited from Photoshop assumes a native app with direct memory access. So the claim "runs entirely in your browser tab, nothing uploaded" deserves a technical explanation rather than marketing. Here is how the pieces actually fit together.</p>
<h2>The document model: layers as data, not pixels</h2>
<p>The core decision that makes everything else work: <strong>the document is stored as a list of operations, not a flattened bitmap.</strong></p>
<p>A layer is a record:</p>
<pre><code class="language-js">{
  id: "layer-7",
  type: "image" | "text" | "shape" | "brush" | "adjustment",
  source: canvas | params,
  opacity: 0.0–1.0,
  blendMode: "normal" | "multiply" | "screen" | "overlay" | ...,
  mask: maskCanvas | null,
  visible: true
}
</code></pre>
<p>Image layers hold an offscreen canvas. Text, shape, and brush layers hold <em>parameters</em> (string, transform, point arrays). Adjustment layers hold only settings — exposure, contrast, curves — with no pixels of their own.</p>
<p>Because the document is data, edits are reversible by construction. Move a text layer? Change a transform field and re-render. Delete a brush stroke? Splice the stroke list. Nothing is ever "baked in" unless you export.</p>
<h2>Compositing: the render loop</h2>
<p>Each frame (or on-demand after an edit), the editor composites the stack bottom-up:</p>
<pre><code class="language-js">function composite(layers, ctx) {
  for (const layer of layers) {
    if (!layer.visible) continue;

    ctx.globalAlpha = layer.opacity;
    ctx.globalCompositeOperation = layer.blendMode;

    let source = resolveSource(layer); // render params → offscreen canvas

    if (layer.mask) {
      // bake mask into a scratch canvas first
      const masked = scratch(layer.width, layer.height);
      masked.draw(source);
      masked.ctx.globalCompositeOperation = "destination-in";
      masked.ctx.drawImage(layer.mask, 0, 0);
      source = masked.canvas;
    }

    ctx.drawImage(source, layer.x, layer.y);
  }
}
</code></pre>
<p>Two details carry most of the weight:</p>
<p><strong>Blend modes are native.</strong> Canvas 2D's <code>globalCompositeOperation</code> implements the full PDF-12 set — <code>multiply</code>, <code>screen</code>, <code>overlay</code>, <code>soft-light</code>, and friends — with the same per-channel math desktop apps use. There is no need to reimplement blending in JavaScript or WebGL for 8-bit compositing.</p>
<p><strong>Masks are alpha maps.</strong> A mask is just another canvas painted in greyscale. Compositing it with <code>destination-in</code> multiplies the layer's alpha by the mask's luminance-encoded alpha — hide-without-deleting semantics, identical to a layer mask in any raster editor.</p>
<h2>Non-destructive adjustments</h2>
<p>An adjustment layer says "apply these parameters to everything below me at composite time." Implementation options:</p>
<ol>
<li><p><strong>CSS</strong> <code>filter</code> <strong>on the draw call</strong> — <code>ctx.filter = "brightness(1.2) contrast(0.9)"</code> is GPU-accelerated and covers the common corrections.</p>
</li>
<li><p><strong>Pixel passes</strong> for anything <code>filter</code> can't express (curves with custom control points) — an <code>ImageData</code> loop or a fragment shader, cached per layer until its parameters change.</p>
</li>
</ol>
<p>Caching matters: every layer renders to its own cached canvas and only re-renders when <em>its</em> inputs change. Editing layer 8 of 12 re-composites the stack (cheap <code>drawImage</code> calls) without re-rendering the untouched sources.</p>
<h2>The parts that genuinely needed extra work</h2>
<p><strong>HEIC input.</strong> Browsers decode JPEG and PNG natively; iPhone HEIC needs a WASM decoder compiled from libheif. It loads lazily — you pay the ~1MB only when an actual HEIC hits the editor.</p>
<p><strong>Modern export formats.</strong> <code>canvas.toBlob</code> gives PNG, JPEG, and WebP for free. AVIF and JPEG-XL have no native encoder, so those go through WASM encoders — again loaded on demand.</p>
<p><strong>Local AI background removal.</strong> An image-segmentation model runs in-browser (WebGL/WASM inference, no upload). It is the same class of on-device model we benchmarked when comparing client-side background removal approaches — good enough for portraits and product shots, with zero privacy trade-off.</p>
<p><strong>Memory ceilings.</strong> Every layer is a full-size RGBA bitmap: a 4000×3000 image costs ~48MB <em>per layer</em> of source data. This is the honest physical limit of in-tab editing — large multi-layer documents belong on desktop hardware, and there is no clever engineering around device RAM.</p>
<h2>What stays on the server</h2>
<p>Almost nothing, by design. In the <a href="https://toolsmonk.com/tool/photo-editor">ToolsMonk Photo Editor</a> — the production version of this architecture — exactly two features are opt-in server calls: smart crop (sends a downscaled copy) and text-to-image generation (sends the prompt). Open, edit, mask, filter, background-remove, and export all happen on your device, which is the whole point: latency bounded by hardware instead of the network, and client data that never leaves the machine.</p>
<p>The interesting takeaway for anyone building creative browser tools: Canvas 2D got further than most people assume. The platform gap with desktop is no longer compositing math — it's memory and native codecs, and both gaps are narrowing.</p>
]]></content:encoded></item><item><title><![CDATA[Financial Calculators That Compute Everything Locally: EMI Math Without a Backend]]></title><description><![CDATA[Financial calculators look trivial — until you ship one that people actually trust with a loan decision. This post covers what an EMI calculator really…]]></description><link>https://toolsmonk.hashnode.dev/financial-calculators-that-compute-everything-locally-emi-math-without-a-backend</link><guid isPermaLink="true">https://toolsmonk.hashnode.dev/financial-calculators-that-compute-everything-locally-emi-math-without-a-backend</guid><dc:creator><![CDATA[ToolsMonk]]></dc:creator><pubDate>Sat, 10 Oct 2026 10:23:10 GMT</pubDate><content:encoded><![CDATA[<p>Financial calculators look trivial — until you ship one that people actually trust with a loan decision. This post covers what an EMI calculator really computes, the UX details that separate a serious tool from an ad-farm widget, and why all of it belongs client-side.</p>
<h2>The math is one formula — the honesty is in the details</h2>
<p>EMI is the standard amortization formula:</p>
<pre><code class="language-js">function emi(principal, annualRate, months) {
  const r = annualRate / 12 / 100;
  if (r === 0) return principal / months;
  const f = Math.pow(1 + r, months);
  return principal * r * f / (f - 1);
}
</code></pre>
<p>Five lines. But a calculator people trust shows more than a number:</p>
<ul>
<li><p><strong>Total interest vs principal split</strong> — the first moment most borrowers realize a long loan doubles the cost. Showing the interest share as a percentage is more honest than hiding it below the fold.</p>
</li>
<li><p><strong>Amortization awareness</strong> — prepayment changes the schedule, not just the EMI. Even a simple comparison view ("what if 1 year shorter?") changes how people use the number.</p>
</li>
<li><p><strong>Currency and locale formatting</strong> — the same math reads completely differently across locales. Intl.NumberFormat handles this; hand-rolled string code does not.</p>
</li>
</ul>
<h2>UX details that matter more than the formula</h2>
<p><strong>Live results as you type.</strong> A "Calculate" button is a remnant of server-era thinking. The state is tiny — three numbers — so recompute on every keystroke. Debounce nothing; it's microseconds.</p>
<p><strong>Offline as a feature, not an accident.</strong> A calculator has zero server dependency, which means a service worker makes it work on a plane, in a basement, or on a dead connection — exactly when people double-check loan numbers.</p>
<p><strong>"Stays on your device" as a visible promise.</strong> People type real salary and loan figures into these boxes. Showing a small, specific indicator that inputs never leave the browser converts a privacy policy into a UI fact.</p>
<h2>Why client-side is the only right answer here</h2>
<p>There is no model, no file, no heavy compute — so a backend adds latency, cost, and a privacy question for zero benefit. Everything the calculator does fits in one function call. The interesting engineering is presentation: charts for the principal/interest split, sane number formatting, and accessible input semantics.</p>
<p>I build these as part of ToolsMonk — a free suite of 255+ browser tools where the EMI calculator (with payment-breakdown charts, comparison view and multi-currency support), SIP, stock and depreciation calculators all run entirely client-side, on the web and inside the Windows and Android apps.</p>
<h2>Takeaways for your own calculators</h2>
<ul>
<li><p>The formula is commodity; the trust details (breakdown, formatting, live updates) are the product.</p>
</li>
<li><p>Ship it offline — a calculator that needs the network is a widget that chose to be worse.</p>
</li>
<li><p>Show the interest share prominently. Users reward honesty with return visits.</p>
</li>
</ul>
<p>What do you look for in a financial calculator before you trust the number?</p>
]]></content:encoded></item><item><title><![CDATA[In-Browser Image Segmentation: How Client-Side Background Removal Actually Works]]></title><description><![CDATA[A technical breakdown of in-browser image segmentation: U2-Net on WebAssembly, COOP/COEP headers for threads, mask feathering on canvas, and the honest…]]></description><link>https://toolsmonk.hashnode.dev/in-browser-image-segmentation-how-client-side-background-removal-actually-works</link><guid isPermaLink="true">https://toolsmonk.hashnode.dev/in-browser-image-segmentation-how-client-side-background-removal-actually-works</guid><category><![CDATA[JavaScript]]></category><category><![CDATA[AI]]></category><category><![CDATA[webdev]]></category><dc:creator><![CDATA[ToolsMonk]]></dc:creator><pubDate>Sat, 10 Oct 2026 10:12:40 GMT</pubDate><content:encoded><![CDATA[<p>Every background-removal tool used to mean the same trade: upload your photo to someone's server, wait for a round trip, and hope their retention policy matches their marketing. Modern browsers have quietly removed that trade. This is a technical look at how client-side image segmentation works, why it became viable, and where it still struggles.</p>
<h2>The model that changed things</h2>
<p>The workhorse behind in-browser background removal is a class of lightweight segmentation models — U^2-Net derivatives and MobileNet-based variants — exported to ONNX and executed with <strong>ONNX Runtime Web</strong> (or TensorFlow.js) on WebAssembly. A quantized model lands around 40-80 MB, downloads once, and is cached by the browser afterwards.</p>
<p>The pipeline is short:</p>
<ol>
<li><p>Decode the image to an offscreen canvas, downscale to the model's input size (typically 320x320).</p>
</li>
<li><p>Normalize pixel values to the model's expected range and run inference — a segmentation mask comes out as a grayscale alpha map.</p>
</li>
<li><p>Upscale the mask back to full resolution, feather the edges (a small blur on the alpha channel), and composite: original RGB + mask as alpha = cutout PNG.</p>
</li>
</ol>
<p>Step 3 is pure canvas work. The only "AI" is step 2, and it runs on the user's CPU — or GPU, if WebGPU is available.</p>
<h2>Why WebAssembly threads needed a header (the fragile part)</h2>
<p>WASM inference is 2-6x faster with threads, and threads require the page to be <strong>cross-origin isolated</strong> — the COOP/COEP header pair. COEP: credentialless was the pragmatic choice for us because the strict variant (require-corp) breaks every third-party image and CDN script that does not send CORP headers.</p>
<p>The failure modes are invisible: a missing COEP header on one worker route kills SharedArrayBuffer with an empty console error, and the fix is a route pattern that exactly matches your worker paths. This class of deployment bug is the real cost of client-side ML — the model is easy, the headers are not.</p>
<h2>Where client-side segmentation still loses to servers</h2>
<p>Honest assessment, because the trade-offs matter:</p>
<ul>
<li><p><strong>Hair and fur edges</strong> — server models with matting refinement still produce cleaner alpha than a 320px-mask upscaled to 4000px. Feathering hides it at social-media sizes, not in print.</p>
</li>
<li><p><strong>Very large images</strong> — decoding a 10000x10000px photo into a canvas can allocate hundreds of MB; tiling strategies help but add complexity.</p>
</li>
<li><p><strong>Speed on old hardware</strong> — a 2015 laptop without WASM threads will wait several seconds; the same image on a server GPU is sub-second.</p>
</li>
</ul>
<p>The compensating advantage is architectural: the image never leaves the device. For personal photos, ID scans, and client assets, that is not a feature — it is the requirement.</p>
<h2>Seeing it in practice</h2>
<p>I ship this pipeline in ToolsMonk's Background Remover — drag an image in, the ~80 MB model is fetched and cached, inference runs locally, and a transparent PNG downloads. The tool reports "100% private: processed in your browser, never uploaded" because that is literally the execution model. The same suite is available as a Windows app and an Android app for offline use.</p>
<h2>If you're implementing this yourself</h2>
<ul>
<li><p>Start with ONNX Runtime Web and a U2-Netp ONNX export; measure before optimizing.</p>
</li>
<li><p>Put inference in a Worker from day one, and get your COOP/COEP headers right before debugging anything else.</p>
</li>
<li><p>Cache the model with the Cache API — first-load is a one-time cost, but make it explicit to the user.</p>
</li>
<li><p>Test the mask on hair. It will tell you immediately whether your feathering radius is right.</p>
</li>
</ul>
<p>The browser is now a perfectly good ML runtime for single-image workloads. The next time a feature "needs a backend", check whether an 80 MB model and a canvas can do it locally instead — the privacy story writes itself.</p>
]]></content:encoded></item><item><title><![CDATA[Processing PDFs Entirely in the Browser: What I Learned Building a Client-Side PDF Suite]]></title><description><![CDATA[How to build PDF merge, split, compress, sign and OCR features that run entirely in the browser with pdf-lib, pdf.js and Tesseract.js — no uploads, no…]]></description><link>https://toolsmonk.hashnode.dev/processing-pdfs-entirely-in-the-browser-what-i-learned-building-a-client-side-pdf-suite</link><guid isPermaLink="true">https://toolsmonk.hashnode.dev/processing-pdfs-entirely-in-the-browser-what-i-learned-building-a-client-side-pdf-suite</guid><category><![CDATA[JavaScript]]></category><category><![CDATA[pdf]]></category><category><![CDATA[webdev]]></category><dc:creator><![CDATA[ToolsMonk]]></dc:creator><pubDate>Sat, 10 Oct 2026 08:45:09 GMT</pubDate><content:encoded><![CDATA[<p>Every "free" PDF tool site has the same architecture: your file goes up, their server does the work, the result comes back. When I started building a PDF suite that runs entirely client-side, I hit a wall of interesting problems. This post is the practical part of what I learned.</p>
<h2>The stack that makes client-side PDF work possible</h2>
<p>Three libraries carry most of the weight:</p>
<ul>
<li><p><strong>pdf-lib</strong> — creating and modifying PDFs (merge, split, page rotation, form filling). Pure JS, works in every modern browser, no server needed.</p>
</li>
<li><p><strong>pdf.js</strong> (Mozilla) — rendering pages to canvas. This is your viewer and the base for any "edit what you see" UI.</p>
</li>
<li><p><strong>Tesseract.js</strong> — OCR via WebAssembly. Slower than server OCR for big scans, but for a 10-page document it's perfectly usable.</p>
</li>
</ul>
<p>The merge operation is the simplest place to start:</p>
<pre><code class="language-js">import { PDFDocument } from 'pdf-lib';

async function mergePdfs(files) {
  const out = await PDFDocument.create();
  for (const file of files) {
    const src = await PDFDocument.load(await file.arrayBuffer());
    const pages = await out.copyPages(src, src.getPageIndices());
    pages.forEach(p =&gt; out.addPage(p));
  }
  return await out.save();
}
</code></pre>
<p>That's a real merge function — about 10 lines, zero uploads.</p>
<h2>The problems that surprised me</h2>
<p><strong>1. Fonts are the hard part of "edit a PDF".</strong> Embedding a font into a PDF with pdf-lib requires a valid TTF. You cannot reference the user's system fonts, so you ship the fonts you offer. Each font is 50-400KB — the real engineering is which 6 fonts you pick.</p>
<p><strong>2. Compression is really image resampling.</strong> "Compress PDF" almost always means: find the large images inside pages, downsample them, rewrite the document. With pdf.js you can rasterize pages to canvas at a chosen DPI and rebuild — crude, but effective for scan-heavy files.</p>
<p><strong>3. OCR in a worker, or the tab dies.</strong> Tesseract.js will happily block the main thread for seconds. It must go into a Web Worker with a progress callback, or users assume the page froze.</p>
<p><strong>4. Memory, not speed, is the ceiling.</strong> A 300-page PDF as an ArrayBuffer plus rendered canvases will eat gigabytes. Page virtualization (render only visible pages, evict the rest) is not optional.</p>
<p><strong>5. Signatures need canvas, not DOM.</strong> Capture the drawn signature as a transparent PNG canvas, then embed it with pdf-lib's drawImage at the click coordinates. Coordinate mapping between the rendered canvas and PDF user space (origin bottom-left, remember) is where the bugs live.</p>
<h2>Where this runs in production</h2>
<p>I ship these techniques in ToolsMonk, a free suite of 250+ tools where the PDF operations (merge, split, compress, sign, OCR, convert) run client-side, available on the web, Windows and Android.</p>
<h2>Takeaways</h2>
<ul>
<li><p>Client-side PDF processing is production-viable in 2026; pdf-lib + pdf.js + a worker is the core.</p>
</li>
<li><p>Budget your engineering for fonts, memory, and coordinate math — not the PDF spec.</p>
</li>
<li><p>If you're building one, start with merge: it's satisfying, useful, and teaches you the arrayBuffer lifecycle you'll need everywhere else.</p>
</li>
</ul>
<p>What have your experiences been with pdf-lib or pdf.js? I'd genuinely like to compare notes on the memory-management tricks.</p>
]]></content:encoded></item></channel></rss>