<?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: Felix ZHU</title>
    <description>The latest articles on DEV Community by Felix ZHU (@bitearcade).</description>
    <link>https://dev.to/bitearcade</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%2F4119247%2F314be7a5-638d-465e-8ca0-8bf1ae1acb8e.png</url>
      <title>DEV Community: Felix ZHU</title>
      <link>https://dev.to/bitearcade</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bitearcade"/>
    <language>en</language>
    <item>
      <title>A black box is not redaction — and three other things I got wrong building local PDF tools</title>
      <dc:creator>Felix ZHU</dc:creator>
      <pubDate>Thu, 12 Nov 2026 12:28:33 +0000</pubDate>
      <link>https://dev.to/bitearcade/a-black-box-is-not-redaction-and-three-other-things-i-got-wrong-building-local-pdf-tools-1pdg</link>
      <guid>https://dev.to/bitearcade/a-black-box-is-not-redaction-and-three-other-things-i-got-wrong-building-local-pdf-tools-1pdg</guid>
      <description>&lt;p&gt;Take any PDF. Draw a black rectangle over a line of text in a viewer that lets you annotate. Save it. Open it again and drag-select across the rectangle.&lt;/p&gt;

&lt;p&gt;The text comes back.&lt;/p&gt;

&lt;p&gt;That is the entire reason I built a PDF tool site, and it is also the first thing I got wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  A black box is a drawing, not a deletion
&lt;/h2&gt;

&lt;p&gt;An annotation is an object layered on top of the page content. The characters underneath are still there, in the content stream, encoded exactly as they were before you covered them up. Select-all still finds them. Copy still copies them. Any text extractor still returns them.&lt;/p&gt;

&lt;p&gt;Actual redaction means the characters stop existing in the file. Not hidden, not covered, not rendered white-on-white — gone from the content stream, so that extraction returns nothing.&lt;/p&gt;

&lt;p&gt;I built the redaction tool around that distinction, and then I made the checker report a number instead of a picture. Counting characters inside the marked areas turns out to be the only way to tell a real deletion from a good-looking one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three different zeros
&lt;/h2&gt;

&lt;p&gt;This is the part that took me longest to get right, and it is the part most redaction tools skip.&lt;/p&gt;

&lt;p&gt;If the checker says "0 characters inside the marked areas," that sentence can mean three completely different things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The text is gone.&lt;/strong&gt; The page had a text layer, you marked it, and now extraction returns 0. This is the result you want, and the report calls it &lt;code&gt;CLEARED&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There was never a text layer.&lt;/strong&gt; The page is a scan. Every pixel is an image; there are no characters to find in the first place. The tool reports &lt;code&gt;NOTHING TO MEASURE&lt;/code&gt;, because the 0 is the absence of a measurement, not a clean result. A scanner output can look identical to a properly redacted page and mean the opposite thing — one has no text because it never did, the other has no text because you removed it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You drew the box on empty space.&lt;/strong&gt; Same 0, same false comfort. The text you meant to remove is three lines down.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first version of my checker merged all three into a single "0" with a green tick. That is a security product telling you what you want to hear. The current version reports the source page's character count next to the output's, so a scan and a clean redaction can never look the same.&lt;/p&gt;

&lt;p&gt;There is a fourth result, and it is the ugly one: the marked page still hands back extractable text after processing. The report says so in plain words and tells you to treat the output like the original. It has happened in testing. I would rather the tool say that than round up to success.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually happens to the file
&lt;/h2&gt;

&lt;p&gt;The redaction runs locally: the page is rendered to a canvas at 2× (about 144 DPI), the marked pages are rebuilt as JPEGs at quality 0.9, and the result is assembled into a new PDF. Pages you did not mark are copied through with their original text layer intact.&lt;/p&gt;

&lt;p&gt;That has a consequence the tool states on the report rather than hides: &lt;strong&gt;a rebuilt page is an image, so everything on it stops being selectable — including the text that was not inside a box.&lt;/strong&gt; If you need the rest of the page to stay searchable, you cannot have that and a destroyed text layer on the same page. Pick which one matters for this document.&lt;/p&gt;

&lt;h2&gt;
  
  
  The leak nobody looks at
&lt;/h2&gt;

&lt;p&gt;Redaction is about what is visible. Metadata is about what is attached.&lt;/p&gt;

&lt;p&gt;I stripped Title, Author, Subject and friends, and the file still carried the old title. The cause: &lt;code&gt;pdf-lib&lt;/code&gt; removes the catalog's reference to the XMP packet, but the packet itself survives as an unreferenced indirect object inside the document. Delete the pointer and the body stays.&lt;/p&gt;

&lt;p&gt;The second mistake was the check. I searched the output bytes for &lt;code&gt;x:xmpmeta&lt;/code&gt; and found nothing, so I called it clean. The object was compressed. A byte search over a deflated stream tells you nothing at all — and this is exactly the kind of false pass that makes a verification step worse than no verification step.&lt;/p&gt;

&lt;p&gt;The fix was to enumerate every indirect object after stripping, decompress each stream (UTF-8 first, then &lt;code&gt;deflate&lt;/code&gt;, then raw deflate), and count the ones that still contain an XMP packet. Streams over 5 MB are reported as &lt;code&gt;unscanned&lt;/code&gt; rather than silently skipped, because "I did not check this" and "this is clean" are different answers.&lt;/p&gt;

&lt;p&gt;On my test file: 1,634 bytes in, 766 out, no XMP left. Compressed variant: 1,256 to 754.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two more surprises, both about what tools quietly drop
&lt;/h2&gt;

&lt;p&gt;Rebuilding a PDF is a destructive operation and the destructive part is not always where you expect.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;PDFDocument.create()&lt;/code&gt; plus &lt;code&gt;copyPages&lt;/code&gt; does not carry Author, Title or XMP into the new document — so a merge tool built this way produces a file with the &lt;em&gt;contents&lt;/em&gt; of both inputs and the metadata of neither. It also stamps its own &lt;code&gt;Producer&lt;/code&gt; and &lt;code&gt;Creator&lt;/code&gt; as &lt;code&gt;pdf-lib (...)&lt;/code&gt;. If you were merging to preserve a document's provenance, you did not.&lt;/p&gt;

&lt;p&gt;Same for splitting: each part is a fresh document, so each part arrives with no author and no title, whatever the original had. A tool page claiming otherwise is a page contradicting its own code.&lt;/p&gt;

&lt;p&gt;The page-level limits are equally unglamorous: splitting one page per file caps at 50 pages, converting to JPG caps at 100, redaction caps at 50, and every tool caps the upload at 100 MB. These are browser limits, not policy — past that point memory pressure starts killing tabs, and a tool that crashes halfway is worse than one that refuses up front.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to check that nothing is uploaded
&lt;/h2&gt;

&lt;p&gt;This is the claim everything else rests on, so it should be falsifiable in ten seconds rather than taken on trust.&lt;/p&gt;

&lt;p&gt;Open the tool, open the browser's Network tab, and drop a file in. You will see no request carrying your document. What you will see is static assets, and Cloudflare's analytics beacon, which the host injects into every page it serves — that is the CDN counting visits, not your file going anywhere. Clear the log, process the file, and watch it stay empty.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five tools, all of them local
&lt;/h2&gt;

&lt;p&gt;Everything above is implemented in the browser with &lt;code&gt;pdf-lib&lt;/code&gt; and &lt;code&gt;pdf.js&lt;/code&gt;. No server, no queue, no upload step, because there is nothing to upload to.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://privacypdf.org/redact-pdf/" rel="noopener noreferrer"&gt;Redact a PDF without uploading it&lt;/a&gt; — draw boxes, rebuild the marked pages as images, get a report that separates the three zeros. Not a certification, not legal advice, and not a substitute for a compliance workflow that requires one.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://privacypdf.org/remove-pdf-metadata/" rel="noopener noreferrer"&gt;Strip metadata from a PDF&lt;/a&gt; — Info dictionary and XMP, verified by decompressing every object rather than trusting the pointer deletion.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://privacypdf.org/merge-pdf/" rel="noopener noreferrer"&gt;Merge PDFs in the browser&lt;/a&gt; — page contents carried, metadata deliberately not.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://privacypdf.org/split-pdf/" rel="noopener noreferrer"&gt;Split a PDF into single pages&lt;/a&gt; — one file per page or fixed ranges, zero-padded names so &lt;code&gt;part10&lt;/code&gt; sorts after &lt;code&gt;part9&lt;/code&gt; and not before &lt;code&gt;part2&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://privacypdf.org/pdf-to-jpg/" rel="noopener noreferrer"&gt;Convert PDF pages to JPG&lt;/a&gt; — 144 or 216 DPI, quality 0.92 fixed, one download per page or a single ZIP.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Disclosure: these are my own tools. No affiliate links, nothing paid, no account required.&lt;/p&gt;

&lt;p&gt;If you need redaction that survives opposing counsel, this is not that product, and I would rather say so here than in a support email. What it is: a local check you can run before you send a file you are not sure about.&lt;/p&gt;

</description>
      <category>pdf</category>
      <category>privacy</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>PDF to JPG in the browser: three problems the tutorials skip</title>
      <dc:creator>Felix ZHU</dc:creator>
      <pubDate>Sun, 08 Nov 2026 10:16:50 +0000</pubDate>
      <link>https://dev.to/bitearcade/pdf-to-jpg-in-the-browser-three-problems-the-tutorials-skip-3a2m</link>
      <guid>https://dev.to/bitearcade/pdf-to-jpg-in-the-browser-three-problems-the-tutorials-skip-3a2m</guid>
      <description>&lt;p&gt;Last month I had to turn a 40-page PDF into images. The reason was embarrassingly small: a form that only accepted JPG. Print to PDF had gotten me a document I could read but not upload, and every converter I found wanted the file on their server first.&lt;/p&gt;

&lt;p&gt;So I built the conversion into a tool that already runs entirely in the browser. The happy path is about twenty lines of code. The rest of this post is the three things that broke after those twenty lines worked, all of which I only found by actually running it on real documents.&lt;/p&gt;

&lt;h2&gt;
  
  
  The twenty lines
&lt;/h2&gt;

&lt;p&gt;pdf.js renders a page onto a canvas, the canvas becomes a blob, the blob becomes a file.&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;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="nx"&gt;bytes&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="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;1&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;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;numPages&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;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="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;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="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;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;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="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="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;blob&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;res&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;res&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/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.92&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;p&gt;That produces a file. It also produces a file that is wrong in three ways I did not notice until I opened one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem one: your JPEG has a black background
&lt;/h2&gt;

&lt;p&gt;This is the one that surprised me most, because it fails silently and looks like a rendering bug.&lt;/p&gt;

&lt;p&gt;A PDF page has no background colour. The page is a set of drawing operations floating on nothing. When pdf.js renders onto a canvas, the areas no content covers keep the canvas's initial state, which is fully transparent — &lt;code&gt;rgba(0, 0, 0, 0)&lt;/code&gt;, not white.&lt;/p&gt;

&lt;p&gt;PNG can store that. JPEG cannot. JPEG has no alpha channel, so when you call &lt;code&gt;toBlob&lt;/code&gt; with &lt;code&gt;image/jpeg&lt;/code&gt;, the browser composites the transparent pixels against black. A white page with two paragraphs of text becomes a black page with two paragraphs of text.&lt;/p&gt;

&lt;p&gt;The fix is one line before rendering:&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;ctx&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;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="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fillStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#ffffff&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillRect&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;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;ctx&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worth noting: this is not a pdf.js defect. It is the correct behaviour for a renderer that does not invent a background the document never declared. But every tutorial I read skipped it, and so did I, until I opened the output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem two: &lt;code&gt;scale&lt;/code&gt; is not a zoom level, it is DPI
&lt;/h2&gt;

&lt;p&gt;The natural reading of &lt;code&gt;getViewport({ scale: 3 })&lt;/code&gt; is "three times bigger". What it actually means is three times the PDF user-space unit, and one user-space unit is 1/72 inch. So:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;scale&lt;/th&gt;
&lt;th&gt;A4 pixel size&lt;/th&gt;
&lt;th&gt;effective DPI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;595 × 842&lt;/td&gt;
&lt;td&gt;72&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1190 × 1684&lt;/td&gt;
&lt;td&gt;144&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;1785 × 2526&lt;/td&gt;
&lt;td&gt;216&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That matters because the number people care about is DPI, not scale. 72 renders fine on screen and looks blurry the moment it is printed or shown on a high-density display. 144 is the everyday setting. 216 is close to print quality and produces files several times larger for the same page.&lt;/p&gt;

&lt;p&gt;My tool exposes these as buttons labelled &lt;code&gt;Small, 72 DPI&lt;/code&gt; / &lt;code&gt;Standard, 144 DPI&lt;/code&gt; / &lt;code&gt;High, 216 DPI&lt;/code&gt; rather than &lt;code&gt;1x / 2x / 3x&lt;/code&gt;, precisely because "3x" tells you nothing about what you are going to get.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem three: canvas fails silently when it gets too big
&lt;/h2&gt;

&lt;p&gt;This is the one that will bite anyone converting large-format documents rather than A4.&lt;/p&gt;

&lt;p&gt;Browsers cap how large a canvas can be. The limits differ by engine and by platform, and — this is the annoying part — an oversized canvas usually does not throw. It returns a blank or partially drawn surface, and your code continues happily, producing empty JPEGs. iOS Safari is considerably stricter than desktop Chrome, so a document that renders correctly on your laptop can come back as solid white rectangles on a phone.&lt;/p&gt;

&lt;p&gt;Two defences are worth having:&lt;/p&gt;

&lt;p&gt;Check the dimensions before you allocate, and refuse the page with a real message instead of producing a blank image. A page 7152 × 10110 (A0 at scale 3) is well past what several browsers will give you.&lt;/p&gt;

&lt;p&gt;Cap the total work. Mine is 100 pages per run. Not because 100 is a magic number, but because the alternative is a tool that accepts a 400-page catalogue and then dies halfway through with no explanation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The memory arithmetic nobody does
&lt;/h2&gt;

&lt;p&gt;A single A4 page at scale 3 allocates &lt;code&gt;1785 × 2526 × 4&lt;/code&gt; bytes — about 17 MB of canvas backing store. Hold all 100 pages and you are at 1.7 GB before a single JPEG exists.&lt;/p&gt;

&lt;p&gt;So pages have to be processed one at a time, and the canvas has to be released before the next one:&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="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="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;height&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then there is the archive. Collecting 100 JPEG blobs and handing them to a ZIP library means the whole output exists twice in memory at once. At an average 2 MB per page that is another 200 MB. On a desktop it survives. On a phone it is the difference between a finished download and a browser that closes the tab.&lt;/p&gt;

&lt;p&gt;The honest answer is that batch size is not a product decision, it is a memory decision. If the limit feels arbitrary, it is — it is set by the smallest device I was willing to support.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the output cannot do, and why I did not fix it
&lt;/h2&gt;

&lt;p&gt;The output is a bitmap. There is no text layer inside it. Nobody can select the words, search them, copy them, or extract them, and a screen reader has nothing to read. If the PDF needs to stay searchable later, converting it to images is the wrong operation and no amount of engineering changes that — the text is being discarded on purpose, because discarding it is the point.&lt;/p&gt;

&lt;p&gt;I also do not let you convert only pages 12 through 20. The tool converts the whole document. A range selector is easy to add and I have not added it, because the UI currently has one file input, one format toggle, one resolution toggle and one button, and every control I add has to be explained to someone who has never thought about DPI. That tradeoff is a choice, not an oversight, and it may change.&lt;/p&gt;

&lt;h2&gt;
  
  
  When not to use this
&lt;/h2&gt;

&lt;p&gt;If the file is going to be printed, 216 DPI and a look at the first page before you commit to the batch.&lt;/p&gt;

&lt;p&gt;If the file has to remain searchable, keep the PDF.&lt;/p&gt;

&lt;p&gt;If it is a scan, converting it changes nothing — it already has no text layer, and you are paying DPI cost for an image you could have had.&lt;/p&gt;

&lt;p&gt;If it is 300 pages, split it first, then convert the parts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tool
&lt;/h2&gt;

&lt;p&gt;I built this as one of six small PDF utilities that all run locally — no upload, no account, no server to trust with the file. The rendering happens in the tab and the file never leaves it.&lt;/p&gt;

&lt;p&gt;If you want to &lt;a href="https://privacypdf.org/pdf-to-jpg/" rel="noopener noreferrer"&gt;convert a PDF to JPG without uploading it&lt;/a&gt;, that is the page. The JPG/PNG toggle, the three DPI settings, and the ZIP download are all there, along with the text above about what the conversion costs you.&lt;/p&gt;

&lt;p&gt;I am the person who wrote it, so treat the recommendation accordingly. The measurement that matters is your own: open the first output file before you run the batch.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>pdf</category>
      <category>webdev</category>
      <category>privacy</category>
    </item>
    <item>
      <title>The black box doesn't delete the text</title>
      <dc:creator>Felix ZHU</dc:creator>
      <pubDate>Sun, 08 Nov 2026 08:52:32 +0000</pubDate>
      <link>https://dev.to/bitearcade/the-black-box-doesnt-delete-the-text-3i1e</link>
      <guid>https://dev.to/bitearcade/the-black-box-doesnt-delete-the-text-3i1e</guid>
      <description>&lt;p&gt;Draw a black rectangle over a name in a PDF, export the file, then reopen it with pdf.js and call &lt;code&gt;getTextContent()&lt;/code&gt;. The name comes back. Word for word, character for character.&lt;/p&gt;

&lt;p&gt;That was the first thing I ran into when I started building a redaction tool that runs entirely in the browser. A black rectangle is a drawing instruction. It sits on a layer above the page. The text object underneath does not know or care that something was painted over it.&lt;/p&gt;

&lt;p&gt;Most tools in this space deal with that in one of two ways. Some rewrite the content stream and genuinely remove the text. Some render the page to an image and throw the text layer away. Both approaches work. The problem is what happens after: the tool says "done", and you have no way to know which of the two just happened, or whether it actually worked on the region you marked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three different zeros
&lt;/h2&gt;

&lt;p&gt;If you build a checker that counts how many characters can still be copied out of the area a user marked, you end up with three situations that all report zero:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The box missed.&lt;/strong&gt; You drew over whitespace. There is no text there, so zero characters is the correct answer to a question nobody meant to ask.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The page is a scan.&lt;/strong&gt; There is no text layer at all. Zero characters is correct, but it tells you nothing about whether the black box covers what you think it covers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It worked.&lt;/strong&gt; The text is gone.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two of those zeros mean "there was nothing to delete". One means "deleted". A tool that only ever reports zero cannot tell you which one you are looking at, and the difference matters a lot: in case 2 you are trusting a rectangle you drew by eye, over an image you cannot read the text of.&lt;/p&gt;

&lt;p&gt;So the check has to say which zero it is, out loud.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to count characters inside a rectangle
&lt;/h2&gt;

&lt;p&gt;pdf.js gives you the text items for a page, but the coordinates are in PDF space, not canvas space. Text position lives in &lt;code&gt;item.transform&lt;/code&gt;, a six-value matrix, and the useful parts are the translation values. &lt;code&gt;Util.transform&lt;/code&gt; converts them into the viewport coordinate system you drew the box in.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Util&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;pdfjs-dist&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;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;doc&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="nx"&gt;pageNumber&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="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;content&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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTextContent&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;charsInBox&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="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;item&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&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;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&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;tx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Util&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transform&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;transform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transform&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;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&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;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&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;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;item&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;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&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;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;item&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;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&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="c1"&gt;// box is { x, y, w, h } in the same top-down viewport space&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;x&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
      &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;charsInBox&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;str&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole idea. Everything else is bookkeeping you have to add around it: which page, which box, how many boxes hit something, and whether the page has any text at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  I made the check a button instead of an automatic step
&lt;/h2&gt;

&lt;p&gt;Upload, redact, check, report. That flow would have been smoother, and I did not build it.&lt;/p&gt;

&lt;p&gt;The check is a button the user presses on purpose. My reasoning: if the tool checks silently, people read the result as "the tool says it is fine" and move on. If someone presses a button and gets back &lt;code&gt;50 characters can still be copied out of 1 marked area.&lt;/code&gt;, they have to decide what to do about that number. It belongs to them, not to a step they did not notice running.&lt;/p&gt;

&lt;p&gt;The same logic applies to the export. After flattening, the tool reopens its own output and counts again. It does not say "done" and stop there. It says &lt;code&gt;210 extractable characters remain, on 2 pages.&lt;/code&gt; or &lt;code&gt;0 extractable characters across 3 pages.&lt;/code&gt; Those are different sentences because they are different outcomes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost of making text stop being copyable
&lt;/h2&gt;

&lt;p&gt;In a browser there is only one reliable way to make text stop being extractable: make the page stop being text.&lt;/p&gt;

&lt;p&gt;If the text object exists in the content stream, pdf.js will find it. So will any other PDF reader, and so will anyone who opens the file with a text editor and looks at the objects. Painting over it does not remove it.&lt;/p&gt;

&lt;p&gt;So the redaction path is: render the marked page at 2x scale, fill the boxes with black, encode the result as JPEG at quality 0.9, then embed that image as a full-page content stream on a fresh page. Pages you did not mark are copied through unchanged.&lt;/p&gt;

&lt;p&gt;The cost of that is real, and it is not small:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;That page is no longer searchable.&lt;/li&gt;
&lt;li&gt;Nobody can select or extract text from it.&lt;/li&gt;
&lt;li&gt;File size goes up, because a JPEG covering a whole page is usually larger than the text it replaced.&lt;/li&gt;
&lt;li&gt;If the page had selectable text that other people needed, that text is gone now.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I put that on the page instead of in a footnote. A redaction tool that hides the tradeoff is asking to be trusted about the wrong thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Numbers from my own test files
&lt;/h2&gt;

&lt;p&gt;Concrete results, not marketing claims.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A 3-page test file, one page marked: the export turned that one page into an image and copied the other two through unchanged. Re-checking the output found &lt;code&gt;210 extractable characters remain, on 2 pages.&lt;/code&gt; That is the honest outcome. I redacted page 1, pages 2 and 3 still have their text, and the checker says so rather than reporting zero.&lt;/li&gt;
&lt;li&gt;The same file with all 3 pages marked: &lt;code&gt;0 extractable characters across 3 pages.&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A 2-page scan with no text layer at all: pressing the check before exporting told me the page had no text layer. It did not report zero characters and leave me guessing.&lt;/li&gt;
&lt;li&gt;A box drawn over empty whitespace: the check said the box missed.&lt;/li&gt;
&lt;li&gt;A box over a line containing a fake name and a fake SSN: &lt;code&gt;50 characters can still be copied out of 1 marked area.&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last number is the one I keep coming back to. Fifty characters is a short line. If a tool had painted a black bar over it and told me the redaction was complete, I would have believed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the other tools do
&lt;/h2&gt;

&lt;p&gt;I looked at the direct competitors before writing any of this. Several are in a similar position: client-side, free, no upload, redaction that happens in the tab.&lt;/p&gt;

&lt;p&gt;The one closest to identical in stack and pitch — same library pair, same "no upload" claim, showing up in search results for these queries — had a toolbar with Open PDF, Undo, Clear All, Prev, Next, Download, and one status message on save: &lt;code&gt;Saved as ... — text layer permanently destroyed.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;No character count. No re-check of the output. No mention of what happens on a scanned page.&lt;/p&gt;

&lt;p&gt;I do not read that as dishonesty. I read it as an unexamined assumption: a box was drawn, so the text must be gone. The assumption holds in most cases and quietly fails in exactly the three cases at the top of this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the check will not tell you
&lt;/h2&gt;

&lt;p&gt;It counts characters that can still be copied out of the regions you marked. It does not tell you that a redaction satisfies any legal, medical, or regulatory standard, and it cannot. What number is low enough for your situation depends on your situation, your counterparty, and possibly your lawyer. A browser tab is not the place to answer that.&lt;/p&gt;

&lt;p&gt;If the number is zero and the page has no text layer, the tool will say the page has no text layer. That is the point: you should be able to tell "nothing left to copy" apart from "nothing there to begin with".&lt;/p&gt;

&lt;p&gt;The tool is at &lt;a href="https://privacypdf.org/redact-pdf" rel="noopener noreferrer"&gt;privacypdf.org/redact-pdf&lt;/a&gt;. It runs in the tab, nothing is uploaded, and there is no account. If you try it, start with the check button: draw a box over something you know is in the file, press Check, and look at the number before you export anything.&lt;/p&gt;

</description>
      <category>pdf</category>
      <category>privacy</category>
      <category>javascript</category>
      <category>security</category>
    </item>
    <item>
      <title>Semrush After the Adobe Acquisition: A 30-Day Paid Review (with two corrections)</title>
      <dc:creator>Felix ZHU</dc:creator>
      <pubDate>Tue, 27 Oct 2026 14:56:45 +0000</pubDate>
      <link>https://dev.to/bitearcade/semrush-after-the-adobe-acquisition-a-30-day-paid-review-with-two-corrections-5dk9</link>
      <guid>https://dev.to/bitearcade/semrush-after-the-adobe-acquisition-a-30-day-paid-review-with-two-corrections-5dk9</guid>
      <description>&lt;h1&gt;
  
  
  Semrush After the Adobe Acquisition: A 30-Day Paid Review
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — Semrush is the most complete all-in-one SEO platform I've used since Adobe closed the $1.9B acquisition in April 2026. The expanded keyword index surfaces genuinely useful long-tail terms, and the new AI Visibility module is the first LLM coverage signal I've gotten from an SEO tool. But the content tools are locked behind the Guru plan, entry is single-seat, and the out-of-the-box audit is so noisy it says more about the crawl settings than about your site.&lt;/p&gt;

&lt;p&gt;This is a paid 30-day test, not a sponsored piece. No vendor contact, no review copy, no free access.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I tested
&lt;/h2&gt;

&lt;p&gt;I ran a paid Semrush subscription for 30 days and used it across three real client projects: a small SEO/content site (14 pages), an e-commerce store (~220 pages), and a B2B SaaS marketing blog (~60 pages).&lt;/p&gt;

&lt;p&gt;I deliberately did &lt;em&gt;not&lt;/em&gt; run the whole toolkit at once. I picked the modules I'd actually use as a solo operator — keyword research, content optimization, site audit, and the new AI Visibility feature — and forced myself to log what each one returned before configuring anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Surprise #1: the keyword index actually surfaced long-tail terms
&lt;/h2&gt;

&lt;p&gt;I assumed the headline index size was vanity marketing. It's not entirely.&lt;/p&gt;

&lt;p&gt;Across the three projects, Semrush surfaced low-volume long-tail terms that my previous tool hadn't shown me — mostly terms with clear search intent that I could target with a dedicated page. On the content site alone, I found 12 non-obvious, low-competition keywords that matched the site's topical coverage but weren't in the other tool's suggestions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The correction I owe you:&lt;/strong&gt; the raw index count is still a vanity number. What matters is how many of those terms have real volume. When I checked a sample of the "new" terms the index surfaced, some had volume and plenty were crawler artifacts. Don't quote the index size as proof of anything — but the coverage itself has genuinely improved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Surprise #2: AI Visibility is the first LLM coverage signal I've seen
&lt;/h2&gt;

&lt;p&gt;The AI Visibility module tracks brand mentions inside ChatGPT, Claude, and Perplexity. For a site that gets meaningful AI-referral traffic, this is the most interesting thing Semrush has shipped this year. It won't tell you &lt;em&gt;why&lt;/em&gt; a model cites you, and it's not deep citation tracking yet — but just knowing whether your brand shows up in a model's answer, and roughly how often, is something no other tool I've used gives me out of the box.&lt;/p&gt;

&lt;p&gt;The module is still early. The signal is directional, not precise, and it treats some verb-based brand names poorly (it confused a client's name with a common verb). I'd use it as a monitoring layer, not a source of truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two corrections I owe this community
&lt;/h2&gt;

&lt;p&gt;I posted an earlier version of this on Reddit and got corrected on two points. Both were right.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The "280B keyword index" framing was overblown.&lt;/strong&gt; As I said above, the raw count is vanity until you filter by real volume. I've stopped leading with the number and now lead with &lt;em&gt;coverage on actual terms&lt;/em&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;"3,800 audit issues on 220 pages" was a bad complaint.&lt;/strong&gt; That number is what a default crawl spits out on a site that size — roughly 17 issues per page, which is unremarkable. Once I adjusted the audit thresholds (excluding cosmetic warnings, focusing on critical + high), the report became useful instead of scary. The noise was my configuration problem, not the tool's.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Where Semrush still falls short
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Content tools are paywalled behind Guru.&lt;/strong&gt; On Pro, the content workflow is thin. You can find keywords, but the full writing/optimization stack — the AI Content Writer and advanced content analytics — sits behind the mid-tier plan. For a solo operator this is the difference between $130/mo and $250/mo for the part of the tool most people actually use daily.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single-seat entry plans.&lt;/strong&gt; There's no cheap solo seat. If you're a one-person site, the entry price buys a lot of modules you may never open.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The audit is noisy out of the box.&lt;/strong&gt; It takes a session to configure thresholds before the site audit tells you something real. That's a learning curve, not a bug — but it's worth budgeting an hour for on setup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pricing creep.&lt;/strong&gt; Annual billing is the only sane option for a solo operator; the gap between Pro and Guru is the real decision point, and it's wide.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Pricing (verified October 2026, from Semrush's official pricing)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Price (annual, per month)&lt;/th&gt;
&lt;th&gt;Who it's for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pro&lt;/td&gt;
&lt;td&gt;~$130&lt;/td&gt;
&lt;td&gt;Solo operators, basic keyword + position tracking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Guru&lt;/td&gt;
&lt;td&gt;~$250&lt;/td&gt;
&lt;td&gt;Teams that need the content toolkit, PPC, and social&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Business / Enterprise&lt;/td&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;td&gt;Agencies and larger orgs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The honest math for a solo operator: Pro gets you the data layer, but the content layer — the part that justifies the subscription for most content marketers — lives at Guru.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who this is for / who it isn't
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Good fit:&lt;/strong&gt; content teams and SEOs who want one source of truth for keyword research, rank tracking, and content monitoring, and who are willing to configure the audit once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not a good fit:&lt;/strong&gt; pure content-only users who just want on-page optimization (Surfer is cheaper and better for that), or anyone producing under 5 articles a month — the ROI doesn't justify the entry price at low volume.&lt;/p&gt;

&lt;h2&gt;
  
  
  Semrush vs. Ahrefs, briefly
&lt;/h2&gt;

&lt;p&gt;The honest comparison: Ahrefs is stronger for backlink analysis and, in my experience, slightly cleaner for pure keyword discovery. Semrush's edge is breadth — you get PPC, social, content, and AI visibility in one place. If you're a one-tool operator covering multiple channels, Semrush wins. If you live and die by link building, Ahrefs is still the specialist.&lt;/p&gt;

&lt;p&gt;For a deeper side-by-side I tested over 30 days, see &lt;a href="https://nicheaitool.com/semrush-vs-surfer-seo" rel="noopener noreferrer"&gt;Semrush vs Surfer SEO&lt;/a&gt; and the full &lt;a href="https://nicheaitool.com/semrush-review" rel="noopener noreferrer"&gt;Semrush review&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom line
&lt;/h2&gt;

&lt;p&gt;Semrush post-acquisition is the most complete all-in-one I've used. "Complete" and "bloat" are the same product — it depends on whether you turn things on deliberately. Configure the audit, filter keywords by real volume, and treat AI Visibility as a directional signal, and it's genuinely useful. Skip that discipline and it's a $250/mo noise generator.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I run a niche SEO tools site and this post links to pages on it, some of which contain affiliate links. This review reflects a paid 30-day test with no vendor sponsorship; opinions are mine and based on what I actually used.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;description: 30 days on a paid Semrush plan after the Adobe deal — two real surprises, two corrections I owe the SEO community, honest pricing, and where it still falls short.&lt;br&gt;
tags: seo, semrush, ahrefs, marketing&lt;br&gt;
canonical_url: &lt;a href="https://nicheaitool.com/semrush-review" rel="noopener noreferrer"&gt;https://nicheaitool.com/semrush-review&lt;/a&gt;&lt;/p&gt;

</description>
      <category>semrush</category>
      <category>ahrefs</category>
      <category>seo</category>
      <category>marketing</category>
    </item>
    <item>
      <title>I Tested Semrush for 30 Days After the $1.9B Adobe Acquisition — Here's What Actually Changed</title>
      <dc:creator>Felix ZHU</dc:creator>
      <pubDate>Sun, 25 Oct 2026 02:08:04 +0000</pubDate>
      <link>https://dev.to/bitearcade/i-tested-semrush-for-30-days-after-the-19b-adobe-acquisition-heres-what-actually-changed-5d38</link>
      <guid>https://dev.to/bitearcade/i-tested-semrush-for-30-days-after-the-19b-adobe-acquisition-heres-what-actually-changed-5d38</guid>
      <description>&lt;p&gt;Adobe closed a $1.9B acquisition of Semrush in April 2026. As a solo developer who does SEO, PPC and content for client sites, I wanted to know if the "new Semrush" was worth the premium pricing — so I ran a 30-day test across real projects: site audits, keyword gap analysis, PPC research, social tracking and content scoring.&lt;/p&gt;

&lt;p&gt;What actually changed post-acquisition:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keyword database jumped to 280B&lt;/strong&gt; (from ~24B), and it's now the largest in the category.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Visibility&lt;/strong&gt; — tracks your brand mentions across ChatGPT, Claude and Perplexity (31M+ prompts monitored). I confirmed this surfaces real mentions I couldn't find with a manual search.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP connectors&lt;/strong&gt; — native Semrush data inside Claude/ChatGPT/Perplexity. This is genuinely new; no competitor ships this out of the box yet.&lt;/li&gt;
&lt;li&gt;Content tools and historical data are still locked behind the Guru plan ($249.95/mo), which is a real cost wall.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The honest cons: entry plans are single-seat (even Business starts with 1 user), a 220-post site triggered 3,800+ audit issues, and the AI writing output still needs heavy editing. And there's no self-serve cancellation.&lt;/p&gt;

&lt;p&gt;If you're weighing Semrush vs Ahrefs for an all-in-one stack, I wrote up the full 30-day breakdown with per-module scores at [&lt;a href="https://nicheaitool.com/semrush-review" rel="noopener noreferrer"&gt;https://nicheaitool.com/semrush-review&lt;/a&gt;] — including who should skip it.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>semrush</category>
      <category>saas</category>
      <category>adobe</category>
    </item>
    <item>
      <title>30+ free SaaS calculators for founders — revenue, CAC, churn, LTV:CAC, ROAS, conversion rate, and more. All run in your browser. No sign-up, no data collection, no tracking. Built because I was tired of blog posts that say "3:1 LTV:CAC is good"</title>
      <dc:creator>Felix ZHU</dc:creator>
      <pubDate>Sun, 11 Oct 2026 05:31:38 +0000</pubDate>
      <link>https://dev.to/bitearcade/30-free-saas-calculators-for-founders-revenue-cac-churn-ltvcac-roas-conversion-rate-and-298m</link>
      <guid>https://dev.to/bitearcade/30-free-saas-calculators-for-founders-revenue-cac-churn-ltvcac-roas-conversion-rate-and-298m</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/bitearcade/30-free-saas-calculators-and-tools-for-founders-2026-1l3m" class="crayons-story__hidden-navigation-link"&gt;30+ Free SaaS Calculators and Tools for Founders (2026)&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/bitearcade" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4119247%2F314be7a5-638d-465e-8ca0-8bf1ae1acb8e.png" alt="bitearcade profile" class="crayons-avatar__image" width="800" height="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/bitearcade" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Felix ZHU
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Felix ZHU
                
                
              
              &lt;div id="story-author-preview-content-4831357" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/bitearcade" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4119247%2F314be7a5-638d-465e-8ca0-8bf1ae1acb8e.png" class="crayons-avatar__image" alt="" width="800" height="800"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Felix ZHU&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/bitearcade/30-free-saas-calculators-and-tools-for-founders-2026-1l3m" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Oct 11&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/bitearcade/30-free-saas-calculators-and-tools-for-founders-2026-1l3m" id="article-link-4831357"&gt;
          30+ Free SaaS Calculators and Tools for Founders (2026)
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/sass"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;sass&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/calculator"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;calculator&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/productivity"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;productivity&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/marketing"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;marketing&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/bitearcade/30-free-saas-calculators-and-tools-for-founders-2026-1l3m" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;1&lt;span class="hidden s:inline"&gt;&amp;nbsp;reaction&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/bitearcade/30-free-saas-calculators-and-tools-for-founders-2026-1l3m#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            4 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>saas</category>
      <category>startup</category>
      <category>tools</category>
    </item>
    <item>
      <title>30+ Free SaaS Calculators and Tools for Founders (2026)</title>
      <dc:creator>Felix ZHU</dc:creator>
      <pubDate>Sun, 11 Oct 2026 05:29:35 +0000</pubDate>
      <link>https://dev.to/bitearcade/30-free-saas-calculators-and-tools-for-founders-2026-1l3m</link>
      <guid>https://dev.to/bitearcade/30-free-saas-calculators-and-tools-for-founders-2026-1l3m</guid>
      <description>&lt;p&gt;Every founder I know has the same problem: you know you should track your metrics, but pulling the numbers together takes forever.&lt;/p&gt;

&lt;p&gt;You open a spreadsheet. You Google "what's a good LTV:CAC ratio." You find three blog posts with three different answers. You close the spreadsheet. Sound familiar?&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://nicheaitool.com" rel="noopener noreferrer"&gt;Niche AI Tool&lt;/a&gt; because I was tired of this cycle. Instead of writing another opinion blog post, I built free calculators that actually solve the problem. All of them run 100% in your browser — no sign-up, no data collection, no tracking.&lt;/p&gt;

&lt;p&gt;Here's the full collection.&lt;/p&gt;




&lt;h2&gt;
  
  
  Revenue &amp;amp; Metrics Calculators
&lt;/h2&gt;

&lt;p&gt;The core stuff. If you only use three tools from this list, make it these three.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://nicheaitool.com/calculator.html" rel="noopener noreferrer"&gt;SaaS Revenue Calculator&lt;/a&gt;&lt;/strong&gt; — MRR, ARR, runway, growth rate. The starting point for any SaaS metrics check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://nicheaitool.com/mrr-growth-calculator.html" rel="noopener noreferrer"&gt;MRR Growth Calculator&lt;/a&gt;&lt;/strong&gt; — Month-over-month growth with CAGR. See if your growth is accelerating or decelerating.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://nicheaitool.com/cac-calculator.html" rel="noopener noreferrer"&gt;CAC Calculator&lt;/a&gt;&lt;/strong&gt; — Customer acquisition cost with payback period. Find out how long it takes to earn back what you spent to get a customer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://nicheaitool.com/churn-rate-calculator.html" rel="noopener noreferrer"&gt;Churn Rate Calculator&lt;/a&gt;&lt;/strong&gt; — Monthly churn with revenue impact. A 5% churn rate doesn't sound bad until you see it means 46% annual churn.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://nicheaitool.com/ltv-cac-ratio-calculator.html" rel="noopener noreferrer"&gt;LTV:CAC Ratio Calculator&lt;/a&gt;&lt;/strong&gt; — The magic ratio every SaaS founder needs. 3:1 is the floor, not the goal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://nicheaitool.com/profit-margin-calculator.html" rel="noopener noreferrer"&gt;Profit Margin Calculator&lt;/a&gt;&lt;/strong&gt; — Gross vs net margin breakdown. See where your money is actually going.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://nicheaitool.com/conversion-rate-calculator.html" rel="noopener noreferrer"&gt;Conversion Rate Calculator&lt;/a&gt;&lt;/strong&gt; — Website conversion rate, CPA, and revenue per visitor. Includes industry benchmarks (e-commerce 2-3%, SaaS 5-10%, B2B 2-5%).&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/17Mg1ZIUCjc" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;




&lt;h2&gt;
  
  
  Marketing &amp;amp; Ads Calculators
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://nicheaitool.com/roas-calculator.html" rel="noopener noreferrer"&gt;ROAS Calculator&lt;/a&gt;&lt;/strong&gt; — Return on ad spend with break-even analysis. Find out if your ads are actually profitable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://nicheaitool.com/roi-calculator.html" rel="noopener noreferrer"&gt;ROI Calculator&lt;/a&gt;&lt;/strong&gt; — General-purpose ROI with timeline. Good for any investment decision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://nicheaitool.com/cpm-calculator.html" rel="noopener noreferrer"&gt;CPM Calculator&lt;/a&gt;&lt;/strong&gt; — Ad cost per thousand impressions. Compare ad platforms apples-to-apples.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://nicheaitool.com/email-open-rate-calculator.html" rel="noopener noreferrer"&gt;Email Open Rate Calculator&lt;/a&gt;&lt;/strong&gt; — Benchmark against industry averages. If your open rate is below 15%, something's wrong with your subject lines or list quality.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://nicheaitool.com/utm-link-builder.html" rel="noopener noreferrer"&gt;UTM Link Builder&lt;/a&gt;&lt;/strong&gt; — Campaign URL builder with presets. Stop guessing which channel is driving traffic.&lt;/p&gt;




&lt;h2&gt;
  
  
  AI &amp;amp; Developer Tools
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://nicheaitool.com/ai-tool-roi-calculator.html" rel="noopener noreferrer"&gt;AI Tool ROI Calculator&lt;/a&gt;&lt;/strong&gt; — Are your ChatGPT, Claude, and Cursor subscriptions actually worth it? Input your hourly rate, weekly usage, and subscription stack to see your real ROI. I've had people tell me they cancelled $80/month of unused subscriptions after using this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://nicheaitool.com/stripe-mcp-config-validator.html" rel="noopener noreferrer"&gt;Stripe MCP Config Validator&lt;/a&gt;&lt;/strong&gt; — Paste your MCP JSON, detect secret-key leaks before you deploy. Runs entirely in your browser, so your keys never leave your device.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://nicheaitool.com/cold-email-roi-calculator.html" rel="noopener noreferrer"&gt;Cold Email ROI Calculator&lt;/a&gt;&lt;/strong&gt; — See if your cold email campaigns are actually profitable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://nicheaitool.com/cold-email-reply-rate-calculator.html" rel="noopener noreferrer"&gt;Cold Email Reply Rate Calculator&lt;/a&gt;&lt;/strong&gt; — Benchmark your reply rates against industry data.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reviews &amp;amp; Comparisons
&lt;/h2&gt;

&lt;p&gt;Calculators are great for your own metrics, but what about the tools you're paying for?&lt;/p&gt;

&lt;p&gt;I also publish data-backed reviews — every one aggregates thousands of real user ratings from G2, Capterra, and Trustpilot. No paid placements, no sponsored rankings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Head-to-head comparisons:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://nicheaitool.com/lemlist-vs-instantly.html" rel="noopener noreferrer"&gt;Lemlist vs Instantly&lt;/a&gt; — Which cold email tool wins in 2026?&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nicheaitool.com/jasper-vs-copy-ai.html" rel="noopener noreferrer"&gt;Jasper vs Copy.ai&lt;/a&gt; — AI writing tools compared&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nicheaitool.com/ahrefs-vs-semrush.html" rel="noopener noreferrer"&gt;Ahrefs vs Semrush&lt;/a&gt; — SEO tools showdown&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Deep reviews:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://nicheaitool.com/jasper-review.html" rel="noopener noreferrer"&gt;Jasper Review&lt;/a&gt; — 4.7/5 G2, but 3.5/5 Trustpilot. The rating gap tells the real story.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nicheaitool.com/instantly-review.html" rel="noopener noreferrer"&gt;Instantly.ai Review&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nicheaitool.com/smartlead-review.html" rel="noopener noreferrer"&gt;Smartlead Review&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why All Free?
&lt;/h2&gt;

&lt;p&gt;Good question. Here's the honest answer:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Tool pages rank better than blog posts.&lt;/strong&gt; People search for calculators, not opinions. Google knows this and rewards pages that satisfy the query completely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Engagement is 10x higher.&lt;/strong&gt; People spend 2-3 minutes using a calculator. They spend 30 seconds skimming a blog post.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No backend = no cost.&lt;/strong&gt; All calculators are pure HTML + JavaScript, deployed on Cloudflare Pages for free. No servers, no databases, no maintenance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your data stays yours.&lt;/strong&gt; Every calculator runs locally in your browser. Your revenue numbers, ad spend, and customer counts never touch a server.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Start Here
&lt;/h2&gt;

&lt;p&gt;If you're new, start with the &lt;a href="https://nicheaitool.com/calculator.html" rel="noopener noreferrer"&gt;SaaS Revenue Calculator&lt;/a&gt;. It takes 30 seconds and will tell you more about your business than most dashboard tools.&lt;/p&gt;

&lt;p&gt;Then check your &lt;a href="https://nicheaitool.com/ltv-cac-ratio-calculator.html" rel="noopener noreferrer"&gt;LTV:CAC ratio&lt;/a&gt;. If it's below 3:1, you have a problem that no amount of growth will fix.&lt;/p&gt;

&lt;p&gt;All tools are free, forever. No credit card, no email gate, no bullshit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://nicheaitool.com/calculator.html" rel="noopener noreferrer"&gt;Explore all 30+ free tools →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: This contains affiliate links. If you sign up through these links, we may earn a commission at no extra cost to you.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>sass</category>
      <category>calculator</category>
      <category>productivity</category>
      <category>marketing</category>
    </item>
    <item>
      <title>I Built 14 Free Calculators to Get SEO Traffic — Here's What Worked</title>
      <dc:creator>Felix ZHU</dc:creator>
      <pubDate>Thu, 01 Oct 2026 09:24:08 +0000</pubDate>
      <link>https://dev.to/bitearcade/i-built-14-free-calculators-to-get-seo-traffic-heres-what-worked-3gg7</link>
      <guid>https://dev.to/bitearcade/i-built-14-free-calculators-to-get-seo-traffic-heres-what-worked-3gg7</guid>
      <description>&lt;h2&gt;
  
  
  The Problem: Blogging Takes Too Long
&lt;/h2&gt;

&lt;p&gt;Like every indie hacker, I started with a blog. I wrote 30+ articles about AI tools, spent hours on keyword research, and waited for Google to notice me.&lt;/p&gt;

&lt;p&gt;Six months later? 12 visitors a day. Most of them from my mom clicking to be supportive.&lt;/p&gt;

&lt;p&gt;The problem was obvious: every AI tool review blog says the same thing. "ChatGPT is great for writing. Claude is great for coding. Cursor is great for..." You've read it a hundred times. I was adding noise to a noisy room.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pivot: Tools Instead of Articles
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpbub7h4d93zyoik2diiy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpbub7h4d93zyoik2diiy.png" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I had a simple realization: &lt;strong&gt;people search for calculators, not opinions.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When someone types "SaaS revenue calculator" into Google, they don't want a 2,000-word blog post. They want to type in their numbers and get an answer. And Google knows this — tool pages rank higher because they satisfy the query completely.&lt;/p&gt;

&lt;p&gt;So I stopped writing reviews and started building tiny calculators. Each one took 2-4 hours. Pure HTML + JavaScript, no backend. Deployed on Cloudflare Pages for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built (14 Tools and Counting)
&lt;/h2&gt;

&lt;p&gt;Here's the current lineup:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Revenue &amp;amp; Metrics (the core)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://nicheaitool.com/calculator" rel="noopener noreferrer"&gt;SaaS Revenue Calculator&lt;/a&gt; — MRR, ARR, runway, growth rate&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nicheaitool.com/mrr-growth-calculator" rel="noopener noreferrer"&gt;MRR Growth Calculator&lt;/a&gt; — month-over-month growth with CAGR&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nicheaitool.com/cac-calculator" rel="noopener noreferrer"&gt;CAC Calculator&lt;/a&gt; — customer acquisition cost with payback period&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nicheaitool.com/churn-calculator" rel="noopener noreferrer"&gt;Churn Rate Calculator&lt;/a&gt; — monthly churn with revenue impact&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nicheaitool.com/ltv-cac-calculator" rel="noopener noreferrer"&gt;LTV:CAC Calculator&lt;/a&gt; — the magic ratio every SaaS founder needs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Marketing &amp;amp; Ads&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://nicheaitool.com/roas-calculator" rel="noopener noreferrer"&gt;ROAS Calculator&lt;/a&gt; — return on ad spend with break-even analysis&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nicheaitool.com/roi-calculator" rel="noopener noreferrer"&gt;ROI Calculator&lt;/a&gt; — general-purpose ROI with timeline&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nicheaitool.com/conversion-rate-calculator" rel="noopener noreferrer"&gt;Conversion Rate Calculator&lt;/a&gt; — A/B test significance checker&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nicheaitool.com/email-open-rate-calculator" rel="noopener noreferrer"&gt;Email Open Rate Calculator&lt;/a&gt; — benchmark against industry averages&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nicheaitool.com/profit-margin-calculator" rel="noopener noreferrer"&gt;Profit Margin Calculator&lt;/a&gt; — gross vs net margin breakdown&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nicheaitool.com/cpm-calculator" rel="noopener noreferrer"&gt;CPM Calculator&lt;/a&gt; — ad cost per thousand impressions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;AI &amp;amp; Developer Tools (the new direction)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://nicheaitool.com/ai-tool-roi-calculator" rel="noopener noreferrer"&gt;AI Tool ROI Calculator&lt;/a&gt; — are your ChatGPT/Claude/Cursor subscriptions actually worth it?&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nicheaitool.com/stripe-mcp-config-validator" rel="noopener noreferrer"&gt;Stripe MCP Config Validator&lt;/a&gt; — paste your MCP JSON, detect secret-key leaks before you deploy&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nicheaitool.com/utm-builder" rel="noopener noreferrer"&gt;UTM Builder&lt;/a&gt; — campaign URL builder with presets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one — the AI Tool ROI Calculator — is currently my favorite. Every developer I know is subscribed to 3-5 AI tools and has no idea if they're getting value. The calculator lets you check your hourly rate, weekly AI usage, and subscription stack to see your real ROI. I've had people tell me they cancelled $80/month of unused subscriptions after using it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Results So Far
&lt;/h2&gt;

&lt;p&gt;I won't pretend this is a viral success story. It's a slow burn, but the numbers are moving in the right direction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Traffic:&lt;/strong&gt; From 12 visitors/day to ~150 visitors/day in 3 months&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Top pages:&lt;/strong&gt; The calculators rank on page 1-2 for their target keywords&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bounce rate:&lt;/strong&gt; 35% (people actually use the tools, then stay to browse)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Average session:&lt;/strong&gt; 2:40 (they calculate, then click "More Free Tools")&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key insight: &lt;strong&gt;tool pages have 10x the engagement of blog posts.&lt;/strong&gt; People spend 2-3 minutes actually using a calculator. They spend 30 seconds skimming a blog post. Google sees this engagement and rewards it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned (The Hard Parts)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Interlinking matters more than I thought.&lt;/strong&gt; Every calculator page links to 5-6 related calculators. This creates a "wiki effect" where Google crawls deep into the site. I added a "More Free Tools" grid to every page, and internal traffic doubled within a week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Schema is non-negotiable.&lt;/strong&gt; Every tool page has &lt;code&gt;WebApplication&lt;/code&gt; + &lt;code&gt;FAQPage&lt;/code&gt; JSON-LD. The FAQ schema alone got me rich snippets within 2 weeks. For the MCP validator, I also added &lt;code&gt;HowTo&lt;/code&gt; schema (Paste → Validate → Review), which targets "how to" search queries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Local processing is a selling point.&lt;/strong&gt; Both the AI ROI Calculator and MCP Config Validator run 100% in the browser. I put "🔒 100% Local · No Sign-up" in the hero. People actually care about this — especially for the MCP validator where they're pasting API keys.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Don't build tools nobody searches for.&lt;/strong&gt; I almost built a "invoice number generator" because it was technically easy. Then I checked search volume — basically zero. Every tool I build now starts with a keyword check. If nobody's searching for it, I don't build it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. The "per-tool ROI" feature almost got me in trouble.&lt;/strong&gt; In the AI ROI Calculator, I initially showed "Keep / OK / Review" labels for each subscription. Then I realized: allocating total savings proportionally to cost is mathematically convenient but logically flawed. A $20 tool doesn't necessarily save more time than a $10 tool. I changed the labels to "High / Moderate / Low" and added a big warning: &lt;em&gt;"This is only a rough mathematical approximation. Real-world individual tool ROI cannot be isolated without separate A/B testing."&lt;/em&gt; Honesty &amp;gt; false precision.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Monetization Question
&lt;/h2&gt;

&lt;p&gt;Right now, these are free tools with no direct revenue. The plan is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Traffic first.&lt;/strong&gt; Get to 500+ visitors/day before worrying about monetization.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Affiliate links.&lt;/strong&gt; The calculators naturally mention tools (Stripe, Shopify, AI tools) — I'll add affiliate links where relevant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Premium templates.&lt;/strong&gt; I already sell a &lt;a href="https://nicheaitool.com/stripe-mcp-prompt-pack" rel="noopener noreferrer"&gt;Stripe MCP Prompt Pack&lt;/a&gt; ($19 one-time). The free validator drives traffic to it. More paid templates are coming.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Newsletter.&lt;/strong&gt; A simple "get new free tools monthly" signup. Not launched yet, but the audience is building.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;I'm adding 1-2 calculators per month. The next ones in the pipeline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Break-even calculator&lt;/strong&gt; for new products&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email subject line tester&lt;/strong&gt; (scoring, not generation)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pricing page analyzer&lt;/strong&gt; (paste your pricing, get conversion heuristics)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're an indie hacker struggling with blog traffic, I'd strongly consider the tool-pivot. It's more work upfront (you have to actually build something), but the traffic quality is dramatically higher. People don't just read your tool — they use it, bookmark it, and share it.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>indiehackers</category>
      <category>webdev</category>
      <category>marketing</category>
    </item>
    <item>
      <title>Built a Free SaaS Metrics Calculator — Here's the Math Behind It</title>
      <dc:creator>Felix ZHU</dc:creator>
      <pubDate>Thu, 10 Sep 2026 11:54:59 +0000</pubDate>
      <link>https://dev.to/bitearcade/built-a-free-saas-metrics-calculator-heres-the-math-behind-it-b3l</link>
      <guid>https://dev.to/bitearcade/built-a-free-saas-metrics-calculator-heres-the-math-behind-it-b3l</guid>
      <description>&lt;p&gt;As a developer, I hate spreadsheets.&lt;/p&gt;

&lt;p&gt;Every time I needed to calculate runway, LTV, or CAC payback for a project, I'd open Google Sheets, build a formula, and inevitably mess up the cell references. After the third time I accidentally calculated runway using the wrong burn rate, I decided to build a tool.&lt;/p&gt;

&lt;p&gt;The result: a free SaaS Revenue Calculator that runs entirely in the browser.&lt;/p&gt;

&lt;p&gt;The Math Behind the Metrics&lt;br&gt;
Here's what the calculator computes and how.&lt;/p&gt;

&lt;p&gt;Runway&lt;br&gt;
javascript&lt;br&gt;
// Runway = Cash in bank / Net monthly burn&lt;br&gt;
const runway = cashInBank / netBurn;&lt;/p&gt;

&lt;p&gt;// Net burn = Monthly costs - MRR&lt;br&gt;
const netBurn = monthlyCosts - mrr;&lt;/p&gt;

&lt;p&gt;// If netBurn is negative, you're profitable and runway is infinite&lt;br&gt;
if (netBurn &amp;lt;= 0) return Infinity;&lt;br&gt;
Runway tells you how many months until you run out of cash. If your MRR exceeds operating costs, you're profitable and runway is effectively infinite.&lt;/p&gt;

&lt;p&gt;LTV (Lifetime Value)&lt;br&gt;
javascript&lt;br&gt;
// LTV = ARPU / Monthly churn rate&lt;br&gt;
const arpu = mrr / activeCustomers;&lt;br&gt;
const ltv = arpu / monthlyChurnRate;&lt;br&gt;
LTV is how much revenue an average customer generates before they churn. Higher is better. A healthy LTV:CAC ratio is 3:1 or higher.&lt;/p&gt;

&lt;p&gt;CAC Payback&lt;br&gt;
javascript&lt;br&gt;
// CAC Payback = CAC / (ARPU * Gross Margin)&lt;br&gt;
const cacPayback = cac / (arpu * grossMargin);&lt;br&gt;
CAC payback is how many months it takes to recover acquisition costs. Under 12 months is good. Under 6 months is excellent.&lt;/p&gt;

&lt;p&gt;Why I Built This&lt;br&gt;
I was tired of:&lt;/p&gt;

&lt;p&gt;Manual calculations — Building the same formulas in every new spreadsheet&lt;/p&gt;

&lt;p&gt;Opaque tools — Existing calculators that don't show their math&lt;/p&gt;

&lt;p&gt;Paywalls — Tools that charge $50/month for basic arithmetic&lt;/p&gt;

&lt;p&gt;The calculator is free, runs locally, and shows you exactly how each metric is computed.&lt;/p&gt;

&lt;p&gt;Technical Implementation&lt;br&gt;
The calculator is a single HTML file with vanilla JavaScript. No frameworks, no build step, no dependencies. It runs entirely client-side — your financial data never leaves your browser.&lt;/p&gt;

&lt;p&gt;Key design decisions:&lt;/p&gt;

&lt;p&gt;Real-time updates: Every input change recalculates all metrics instantly&lt;/p&gt;

&lt;p&gt;12-month projection: Uses compound growth to project MRR forward&lt;/p&gt;

&lt;p&gt;Health indicators: Each metric shows a green/yellow/red status based on benchmarks&lt;/p&gt;

&lt;p&gt;When to Use This&lt;br&gt;
Before a fundraise — Investors will ask about LTV:CAC, CAC payback, and net burn. Get your numbers straight.&lt;/p&gt;

&lt;p&gt;When deciding to hire — See how adding a salary affects your runway.&lt;/p&gt;

&lt;p&gt;When evaluating churn — Model how reducing churn from 5% to 3% changes LTV and runway.&lt;/p&gt;

&lt;p&gt;→ Try the calculator&lt;/p&gt;

&lt;p&gt;FAQ&lt;br&gt;
Can I self-host this?&lt;/p&gt;

&lt;p&gt;The calculator is a static HTML file. You can save it locally and run it offline.&lt;/p&gt;

&lt;p&gt;Does it store my data?&lt;/p&gt;

&lt;p&gt;No. Everything runs in your browser. No server calls, no analytics, no storage.&lt;/p&gt;

&lt;p&gt;What's Next&lt;br&gt;
I'm planning to add:&lt;/p&gt;

&lt;p&gt;Scenario comparison (side-by-side what-if analysis)&lt;/p&gt;

&lt;p&gt;Export to CSV/PDF&lt;/p&gt;

&lt;p&gt;Cohort retention modeling&lt;/p&gt;

&lt;p&gt;If you have feature requests, let me know.&lt;/p&gt;

&lt;p&gt;Disclosure: This article contains affiliate links. If you sign up through these links, we may earn a commission at no extra cost to you.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>startup</category>
      <category>javascript</category>
      <category>indiedev</category>
    </item>
  </channel>
</rss>
