<?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: PDFixed</title>
    <description>The latest articles on DEV Community by PDFixed (@pdfixed).</description>
    <link>https://dev.to/pdfixed</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%2F4224960%2F3105a63e-e289-485d-8ad1-4ca1fc23b909.png</url>
      <title>DEV Community: PDFixed</title>
      <link>https://dev.to/pdfixed</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pdfixed"/>
    <language>en</language>
    <item>
      <title>Your PDF never leaves your browser: how I built 16 PDF tools with no uploads</title>
      <dc:creator>PDFixed</dc:creator>
      <pubDate>Tue, 03 Nov 2026 20:28:39 +0000</pubDate>
      <link>https://dev.to/pdfixed/your-pdf-never-leaves-your-browser-how-i-built-16-pdf-tools-with-no-uploads-38bf</link>
      <guid>https://dev.to/pdfixed/your-pdf-never-leaves-your-browser-how-i-built-16-pdf-tools-with-no-uploads-38bf</guid>
      <description>&lt;p&gt;Most "free PDF" sites work the same way: you upload your contract, they process it on a server, you download the result. It works, but your payslip or passport scan just took a trip to a stranger's machine.&lt;/p&gt;

&lt;p&gt;I wanted to see how far a browser can go on its own. The result is &lt;a href="https://pdfixed.com" rel="noopener noreferrer"&gt;PDFixed&lt;/a&gt;: 16 PDF tools where the file never leaves your device. Here is how the main ones work, in case you want to build something similar.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://pdf-lib.js.org/" rel="noopener noreferrer"&gt;pdf-lib&lt;/a&gt;&lt;/strong&gt; to create, merge, split and rewrite PDFs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://mozilla.github.io/pdf.js/" rel="noopener noreferrer"&gt;pdf.js&lt;/a&gt;&lt;/strong&gt; to render pages onto a canvas&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tesseract.projectnaptha.com/" rel="noopener noreferrer"&gt;Tesseract.js&lt;/a&gt;&lt;/strong&gt; for OCR&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alpine.js + Vite&lt;/strong&gt; for the interface&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The free tools have no upload endpoint. The server sends the page and its scripts; everything else runs in the visitor's tab. (There is a separate, optional expert editing service where you send a file on purpose, but that is not one of the tools.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Merge: pdf-lib does the heavy lifting
&lt;/h2&gt;

&lt;p&gt;Merging is the easy one. Copy the pages of each source document into a new 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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PDFDocument&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pdf-lib&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;merged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;PDFDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;PDFDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;merged&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copyPages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPageIndices&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="nx"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;merged&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;merged&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/pdf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last line is the whole "download" step. The result is a &lt;code&gt;Blob&lt;/code&gt; in memory, and a link to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compress: most of a PDF is images
&lt;/h2&gt;

&lt;p&gt;Most of a big PDF's weight is photos and scans, not text. So the compressor finds every embedded image, decodes it, scales it down to a size limit, and saves it again as a JPEG, but only when that actually makes it smaller. Text and vector drawings are untouched, so they stay sharp and selectable. There are three levels, with different maximum image sizes and JPEG quality settings. Then pdf-lib saves the document with compact object streams.&lt;/p&gt;

&lt;p&gt;The "only if smaller" rule matters. A PDF that is already mostly text barely shrinks, and the tool says so instead of pretending.&lt;/p&gt;

&lt;h2&gt;
  
  
  OCR: a scanned PDF is just pictures
&lt;/h2&gt;

&lt;p&gt;A scanned PDF has no text, only an image of each page. To make it searchable:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;pdf.js renders each page to a canvas.&lt;/li&gt;
&lt;li&gt;Tesseract.js reads the canvas and returns the words and their positions.&lt;/li&gt;
&lt;li&gt;pdf-lib adds that text back as an invisible layer on top of the original page.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The page looks identical, but you can now search and copy it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createWorker&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tesseract.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// worker, core and language files are served from our own site&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;worker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createWorker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eng&lt;/span&gt;&lt;span class="dl"&gt;'&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;workerPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;corePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;langPath&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recognize&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I host the Tesseract worker and language files myself, so OCR does not need to call any third-party server either.&lt;/p&gt;

&lt;h2&gt;
  
  
  Protect: AES-256 in the browser
&lt;/h2&gt;

&lt;p&gt;Password protection is just encryption, and the PDF format supports AES-256. A fork of pdf-lib (&lt;code&gt;@cantoo/pdf-lib&lt;/code&gt;) can encrypt a document before saving it. The password is used locally and never sent anywhere, which also means I cannot recover a forgotten one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What did not go smoothly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Big files are limited by the tab's memory.&lt;/strong&gt; Rendering hundreds of pages to canvases is heavy, so there is a file size cap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OCR is slow.&lt;/strong&gt; It takes a few seconds per page, and accuracy depends on how clean the scan is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PDF to Word is approximate.&lt;/strong&gt; I convert text and basic formatting, but tables and pictures are not rebuilt, and I say so in the tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PDF is a strange format.&lt;/strong&gt; Fonts, encodings and broken files will humble you. Always test with real-world PDFs, not only the ones you made yourself.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Check it yourself
&lt;/h2&gt;

&lt;p&gt;Open DevTools, go to the Network tab, drop a file into any tool and watch. You will see the page's scripts load, and no request carrying your document.&lt;/p&gt;

&lt;p&gt;If you try it, I would like to hear which PDF job annoys you most that a tool like this could handle. The tools are free, with no account: &lt;a href="https://pdfixed.com" rel="noopener noreferrer"&gt;pdfixed.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>pdf</category>
    </item>
  </channel>
</rss>
