<?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: James</title>
    <description>The latest articles on DEV Community by James (@privyfiles).</description>
    <link>https://dev.to/privyfiles</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3892844%2F8a1e2493-303a-450f-a479-17a97bb60949.png</url>
      <title>DEV Community: James</title>
      <link>https://dev.to/privyfiles</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/privyfiles"/>
    <language>en</language>
    <item>
      <title>How I Built a Privacy-First PDF Tool Using pdf-lib and the Canvas API</title>
      <dc:creator>James</dc:creator>
      <pubDate>Wed, 22 Apr 2026 16:50:48 +0000</pubDate>
      <link>https://dev.to/privyfiles/how-i-built-a-privacy-first-pdf-tool-using-pdf-lib-and-the-canvas-api-3jm8</link>
      <guid>https://dev.to/privyfiles/how-i-built-a-privacy-first-pdf-tool-using-pdf-lib-and-the-canvas-api-3jm8</guid>
      <description>&lt;p&gt;A few weeks ago I got frustrated with online PDF tools. Not with their functionality — iLovePDF and Smallpdf are genuinely useful. What frustrated me was what happens to your files after you click upload.&lt;br&gt;
I read the privacy policies. Files retained for 24 hours. Cloud processing on remote servers. Advertising tracking on tool pages. For a tool handling potentially sensitive documents — contracts, financial records, medical paperwork — this felt like an unacceptable trade-off.&lt;br&gt;
So I built PrivyFiles. Here's exactly how the privacy architecture works and the technical decisions I made along the way.&lt;br&gt;
The Core Privacy Problem With Cloud PDF Tools&lt;br&gt;
When you upload a file to a typical online PDF tool, this is what happens:&lt;/p&gt;

&lt;p&gt;Your file travels from your browser to a remote server over HTTPS&lt;br&gt;
The server processes the file&lt;br&gt;
The processed file sits on that server waiting for you to download it&lt;br&gt;
The server retains your file for a defined retention period — sometimes hours, sometimes days&lt;/p&gt;

&lt;p&gt;During step 4, your file exists on infrastructure you have no control over. It is subject to that company's security practices, their employees' access controls, and their legal obligations to respond to government requests.&lt;br&gt;
For most files this is fine. For a contract containing your client's personal data, a financial statement, or a document with sensitive business information — it is not fine.&lt;br&gt;
The solution is obvious in theory: process the file in the browser so it never leaves the device.&lt;br&gt;
The Browser-Based Architecture&lt;br&gt;
PrivyFiles uses two main technologies for client-side processing:&lt;br&gt;
pdf-lib for PDF manipulation&lt;br&gt;
Canvas API for image processing&lt;br&gt;
Using pdf-lib for PDF Metadata Removal&lt;br&gt;
pdf-lib is a JavaScript library that allows you to create and modify PDF documents entirely in the browser. For metadata removal specifically, it works like this:&lt;/p&gt;

&lt;p&gt;async function removeMetadata(file) {&lt;br&gt;
  const arrayBuffer = await file.arrayBuffer();&lt;br&gt;
  const pdfDoc = await PDFLib.PDFDocument.load(arrayBuffer);&lt;/p&gt;

&lt;p&gt;pdfDoc.setTitle('');&lt;br&gt;
  pdfDoc.setAuthor('');&lt;br&gt;
  pdfDoc.setSubject('');&lt;br&gt;
  pdfDoc.setKeywords([]);&lt;br&gt;
  pdfDoc.setProducer('');&lt;br&gt;
  pdfDoc.setCreator('');&lt;br&gt;
  pdfDoc.setCreationDate(new Date(0));&lt;br&gt;
  pdfDoc.setModificationDate(new Date(0));&lt;/p&gt;

&lt;p&gt;const pdfBytes = await pdfDoc.save();&lt;br&gt;
  const blob = new Blob([pdfBytes], { type: 'application/pdf' });&lt;br&gt;
  const url = URL.createObjectURL(blob);&lt;/p&gt;

&lt;p&gt;const link = document.createElement('a');&lt;br&gt;
  link.href = url;&lt;br&gt;
  link.download = 'cleaned_' + file.name;&lt;br&gt;
  document.body.appendChild(link);&lt;br&gt;
  link.click();&lt;br&gt;
  document.body.removeChild(link);&lt;br&gt;
  URL.revokeObjectURL(url);&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;The entire operation happens in memory. The file never touches a network request. From a privacy standpoint this is as good as it gets — we literally cannot see the file because it never reaches us.&lt;br&gt;
Using pdf-lib for PDF Rotation&lt;br&gt;
Rotation is another operation that works beautifully client-side:&lt;/p&gt;

&lt;p&gt;async function rotatePDF(file, degrees) {&lt;br&gt;
  const arrayBuffer = await file.arrayBuffer();&lt;br&gt;
  const pdfDoc = await PDFLib.PDFDocument.load(arrayBuffer);&lt;br&gt;
  const pages = pdfDoc.getPages();&lt;/p&gt;

&lt;p&gt;pages.forEach(page =&amp;gt; {&lt;br&gt;
    const currentRotation = page.getRotation().angle;&lt;br&gt;
    page.setRotation(PDFLib.degrees(currentRotation + degrees));&lt;br&gt;
  });&lt;/p&gt;

&lt;p&gt;const pdfBytes = await pdfDoc.save();&lt;br&gt;
  const blob = new Blob([pdfBytes], { type: 'application/pdf' });&lt;br&gt;
  const url = URL.createObjectURL(blob);&lt;/p&gt;

&lt;p&gt;const link = document.createElement('a');&lt;br&gt;
  link.href = url;&lt;br&gt;
  link.download = 'rotated_' + file.name;&lt;br&gt;
  document.body.appendChild(link);&lt;br&gt;
  link.click();&lt;br&gt;
  document.body.removeChild(link);&lt;br&gt;
  URL.revokeObjectURL(url);&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Pass 90 for clockwise, 180 for a full flip, 270 for anticlockwise. Clean and simple.&lt;br&gt;
Using Canvas API for Image EXIF Removal&lt;br&gt;
JPEG images contain EXIF metadata — camera model, GPS coordinates, timestamp, and more. Stripping this data using the Canvas API is elegant:&lt;/p&gt;

&lt;p&gt;async function removeExif(file) {&lt;br&gt;
  const img = new Image();&lt;br&gt;
  const objectUrl = URL.createObjectURL(file);&lt;br&gt;
  img.src = objectUrl;&lt;/p&gt;

&lt;p&gt;await new Promise(resolve =&amp;gt; img.onload = resolve);&lt;/p&gt;

&lt;p&gt;const canvas = document.createElement('canvas');&lt;br&gt;
  canvas.width = img.width;&lt;br&gt;
  canvas.height = img.height;&lt;/p&gt;

&lt;p&gt;const ctx = canvas.getContext('2d');&lt;br&gt;
  ctx.drawImage(img, 0, 0);&lt;br&gt;
  URL.revokeObjectURL(objectUrl);&lt;/p&gt;

&lt;p&gt;canvas.toBlob((blob) =&amp;gt; {&lt;br&gt;
    const link = document.createElement('a');&lt;br&gt;
    link.href = URL.createObjectURL(blob);&lt;br&gt;
    link.download = 'cleaned_' + file.name;&lt;br&gt;
    document.body.appendChild(link);&lt;br&gt;
    link.click();&lt;br&gt;
    document.body.removeChild(link);&lt;br&gt;
  }, file.type, 1.0);&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;When you draw an image to a canvas and export it back as a blob, the browser strips all EXIF data automatically. The pixel data is preserved perfectly. The metadata is gone.&lt;br&gt;
This works because the Canvas API only understands pixel data — it has no concept of EXIF metadata, so it simply does not include it in the output.&lt;br&gt;
The Hybrid Approach — When Server-Side Is Unavoidable&lt;br&gt;
Some conversions simply cannot be done client-side at a quality level users expect. PDF to Word conversion, for example, requires complex document parsing that is not yet practical in the browser.&lt;br&gt;
For these tools, PrivyFiles uses ConvertAPI with a strict 60 second auto-deletion policy. The API call looks like this:&lt;/p&gt;

&lt;p&gt;async function convertFile(file, fromFormat, toFormat) {&lt;br&gt;
  const base64 = await toBase64(file);&lt;/p&gt;

&lt;p&gt;const response = await fetch(&lt;br&gt;
    &lt;code&gt;https://v2.convertapi.com/convert/${fromFormat}/to/${toFormat}&lt;/code&gt;,&lt;br&gt;
    {&lt;br&gt;
      method: 'POST',&lt;br&gt;
      headers: {&lt;br&gt;
        'Authorization': 'Bearer YOUR_TOKEN',&lt;br&gt;
        'Content-Type': 'application/json'&lt;br&gt;
      },&lt;br&gt;
      body: JSON.stringify({&lt;br&gt;
        Parameters: [{&lt;br&gt;
          Name: 'File',&lt;br&gt;
          FileValue: {&lt;br&gt;
            Name: file.name,&lt;br&gt;
            Data: base64&lt;br&gt;
          }&lt;br&gt;
        }]&lt;br&gt;
      })&lt;br&gt;
    }&lt;br&gt;
  );&lt;/p&gt;

&lt;p&gt;const result = await response.json();&lt;/p&gt;

&lt;p&gt;if (result.Files &amp;amp;&amp;amp; result.Files.length &amp;gt; 0) {&lt;br&gt;
    const link = document.createElement('a');&lt;br&gt;
    link.href = 'data:application/octet-stream;base64,' +&lt;br&gt;
      result.Files[0].FileData;&lt;br&gt;
    link.download = result.Files[0].FileName;&lt;br&gt;
    document.body.appendChild(link);&lt;br&gt;
    link.click();&lt;br&gt;
    document.body.removeChild(link);&lt;br&gt;
  }&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;function toBase64(file) {&lt;br&gt;
  return new Promise((resolve, reject) =&amp;gt; {&lt;br&gt;
    const reader = new FileReader();&lt;br&gt;
    reader.readAsDataURL(file);&lt;br&gt;
    reader.onload = () =&amp;gt; resolve(reader.result.split(',')[1]);&lt;br&gt;
    reader.onerror = reject;&lt;br&gt;
  });&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;The privacy trade-off here is clear and disclosed to users. For metadata removal, EXIF stripping, and rotation — fully local. For complex format conversion — server-side with immediate deletion.&lt;br&gt;
The Image Compression Approach&lt;br&gt;
For image compression, the Canvas API again does the heavy lifting:&lt;/p&gt;

&lt;p&gt;async function compressImage(file, quality = 0.7) {&lt;br&gt;
  const img = new Image();&lt;br&gt;
  const objectUrl = URL.createObjectURL(file);&lt;br&gt;
  img.src = objectUrl;&lt;/p&gt;

&lt;p&gt;await new Promise(resolve =&amp;gt; img.onload = resolve);&lt;/p&gt;

&lt;p&gt;const canvas = document.createElement('canvas');&lt;br&gt;
  canvas.width = img.width;&lt;br&gt;
  canvas.height = img.height;&lt;/p&gt;

&lt;p&gt;const ctx = canvas.getContext('2d');&lt;br&gt;
  ctx.drawImage(img, 0, 0);&lt;br&gt;
  URL.revokeObjectURL(objectUrl);&lt;/p&gt;

&lt;p&gt;canvas.toBlob((blob) =&amp;gt; {&lt;br&gt;
    const link = document.createElement('a');&lt;br&gt;
    link.href = URL.createObjectURL(blob);&lt;br&gt;
    link.download = 'compressed_' + file.name;&lt;br&gt;
    document.body.appendChild(link);&lt;br&gt;
    link.click();&lt;br&gt;
    document.body.removeChild(link);&lt;br&gt;
  }, file.type, quality);&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;The quality parameter accepts a value between 0 and 1. We give users three options — Low (0.5), Medium (0.7), and High (0.9) — mapped to a simple dropdown.&lt;br&gt;
What I Learned About Browser-Based File Processing&lt;br&gt;
Memory management matters. For large files, loading everything into memory simultaneously can crash mobile browsers. Always use URL.revokeObjectURL() immediately after use and avoid holding multiple large ArrayBuffers simultaneously.&lt;br&gt;
pdf-lib has limitations. It cannot reorder pages in complex PDFs with cross-references, and some encrypted PDFs refuse to load. Always handle errors gracefully and fall back to a clear error message.&lt;br&gt;
The Canvas approach has a quality trade-off. Drawing to canvas and exporting as JPEG always applies some compression even at quality 1.0. For lossless PNG output use image/png as the MIME type in toBlob().&lt;br&gt;
Users trust architecture more than policy. Telling users "we delete your files" is a policy claim. Showing them that the file never leaves their browser is an architectural guarantee. The difference matters to privacy-conscious users.&lt;br&gt;
The Result&lt;br&gt;
PrivyFiles launched 4 days ago with zero paid promotion and has already received 473 organic visitors — almost entirely from people searching for privacy-focused PDF tools. The privacy angle resonates strongly with users who have grown frustrated with mainstream tools.&lt;br&gt;
The site is at privyfiles.com if you want to check it out. All feedback welcome — especially from developers who spot ways to improve the client-side architecture.&lt;br&gt;
The metadata removal, EXIF stripping, rotation, and image compression tools are all fully client-side. Give them a try and check your browser's network tab — you will see zero file upload requests.&lt;br&gt;
That is the privacy guarantee that no policy document can match.&lt;/p&gt;

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