<?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: Akshay Madhan</title>
    <description>The latest articles on DEV Community by Akshay Madhan (@akshaymadhan).</description>
    <link>https://dev.to/akshaymadhan</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%2F111460%2F02611bf6-cb42-456e-afbb-1574f764e075.png</url>
      <title>DEV Community: Akshay Madhan</title>
      <link>https://dev.to/akshaymadhan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/akshaymadhan"/>
    <language>en</language>
    <item>
      <title>I stopped uploading PDFs to random websites — here's the browser-only version</title>
      <dc:creator>Akshay Madhan</dc:creator>
      <pubDate>Tue, 08 Sep 2026 06:38:39 +0000</pubDate>
      <link>https://dev.to/akshaymadhan/i-stopped-uploading-pdfs-to-random-websites-heres-the-browser-only-version-3m72</link>
      <guid>https://dev.to/akshaymadhan/i-stopped-uploading-pdfs-to-random-websites-heres-the-browser-only-version-3m72</guid>
      <description>&lt;p&gt;I kept needing a PNG of one PDF page.&lt;/p&gt;

&lt;p&gt;Every site I tried wanted the file uploaded first. For a tax form or a contract, that felt wrong. So I built the conversion in the browser instead.&lt;/p&gt;

&lt;p&gt;No backend job. No "we're processing your file." The PDF stays in the tab, gets drawn to a canvas, and you download a PNG.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual problem
&lt;/h2&gt;

&lt;p&gt;Most "PDF to PNG" tools are a form + a server.&lt;/p&gt;

&lt;p&gt;That is fine for a flyer. It is not fine when the PDF has an address, a passport scan, or an invoice. &lt;a href="https://andergrove.com/posts/are-online-pdf-tools-safe/" rel="noopener noreferrer"&gt;Andergrove&lt;/a&gt; puts it bluntly: never upload a document you would not email to a stranger. Once you press upload, copies exist on someone else's disk until &lt;em&gt;they&lt;/em&gt; delete them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://privapdf.net/guides/are-online-pdf-tools-safe" rel="noopener noreferrer"&gt;PrivaPDF's write-up&lt;/a&gt; has the test I now run on any "free converter": open DevTools → Network, drop a file, and watch for a &lt;code&gt;POST&lt;/code&gt;. If the file leaves the tab, it is not local.&lt;/p&gt;

&lt;p&gt;A browser can already do this. Mozilla's &lt;a href="https://mozilla.github.io/pdf.js/" rel="noopener noreferrer"&gt;PDF.js&lt;/a&gt; can parse the file, &lt;a href="https://mozilla.github.io/pdf.js/api/draft/module-pdfjsLib-PDFPageProxy.html" rel="noopener noreferrer"&gt;&lt;code&gt;page.render()&lt;/code&gt;&lt;/a&gt; can paint a page onto a canvas, and you export a PNG. The server never sees the bytes.&lt;/p&gt;

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

&lt;p&gt;The official &lt;a href="https://mozilla.github.io/pdf.js/examples/" rel="noopener noreferrer"&gt;PDF.js examples&lt;/a&gt; are the skeleton. The only extra is feeding &lt;code&gt;getDocument&lt;/code&gt; the bytes from a &lt;code&gt;&amp;lt;input type="file"&amp;gt;&lt;/code&gt; instead of a URL (&lt;a href="https://mozilla.github.io/pdf.js/api/draft/module-pdfjsLib.html" rel="noopener noreferrer"&gt;&lt;code&gt;getDocument&lt;/code&gt; docs&lt;/a&gt;):&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;pdfjsLib&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pdf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pdfjsLib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDocument&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="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="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;promise&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;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;scale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dpi&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;72&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="nx"&gt;scale&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="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="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="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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;png&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toDataURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image/png&lt;/span&gt;&lt;span class="dl"&gt;'&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;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;dpi / 72&lt;/code&gt; is the only scale math that matters. Adobe's own docs say default &lt;a href="https://opensource.adobe.com/dc-acrobat-sdk-docs/library/interapp/IAC_API_CoordinateSystems.html" rel="noopener noreferrer"&gt;user space is 1/72 of an inch&lt;/a&gt;. PDF.js says the same thing in the examples: the viewport at scale &lt;code&gt;1&lt;/code&gt; is &lt;strong&gt;72 DPI&lt;/strong&gt;. So &lt;code&gt;150 / 72&lt;/code&gt; is "render this page at 150 DPI." ISO 32000 calls that unit a user unit; &lt;a href="https://kb.itextpdf.com/itext/how-do-the-measurement-systems-in-html-relate-to-t" rel="noopener noreferrer"&gt;iText has a readable walkthrough&lt;/a&gt; if you want the spec language.&lt;/p&gt;

&lt;p&gt;150 DPI is sharp on a screen without melting a phone.&lt;/p&gt;

&lt;p&gt;Point &lt;code&gt;GlobalWorkerOptions.workerSrc&lt;/code&gt; at &lt;code&gt;pdfjs-dist/build/pdf.worker.min.mjs&lt;/code&gt; or the main thread will stall on a 20-page file. The &lt;a href="https://github.com/mozilla/pdf.js/wiki/Frequently-Asked-Questions" rel="noopener noreferrer"&gt;PDF.js FAQ&lt;/a&gt; exists because everyone forgets this once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things I hit that the happy-path tutorial skips
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The file has to stay in memory.&lt;/strong&gt; A React state object with &lt;code&gt;fileName: "report.pdf"&lt;/code&gt; is not a PDF. If you only persist the name, you will render a fake page and the user will download the wrong PNG. Keep the &lt;code&gt;File&lt;/code&gt; in a ref. &lt;code&gt;getDocument&lt;/code&gt; needs the bytes (&lt;a href="https://mozilla.github.io/pdf.js/api/draft/module-pdfjsLib.html" rel="noopener noreferrer"&gt;API&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;High DPI will OOM the tab.&lt;/strong&gt; Cap the long side (I use 2400px). 300 DPI on A4 is already a huge canvas. 50 pages at that size will lock Chrome. Chromium even has an internal GPU budget for canvases so the compositor does not starve (&lt;a href="https://chromium.googlesource.com/chromium/src/+/e679eb591b3d49ee08cacb83bf4b3e2d5cc4a80e/third_party/WebKit/Source/core/html/HTMLCanvasElement.cpp" rel="noopener noreferrer"&gt;&lt;code&gt;HTMLCanvasElement.cpp&lt;/code&gt;&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Page count comes from the PDF, not the UI.&lt;/strong&gt; &lt;code&gt;pdf.numPages&lt;/code&gt; is the source of truth. A leftover default of &lt;code&gt;3&lt;/code&gt; will convert the wrong range.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transparent background is a lie on most real PDFs.&lt;/strong&gt; If the page already painted white, &lt;code&gt;clearRect&lt;/code&gt; does nothing useful. PDF.js lets you pass &lt;code&gt;background: 'transparent'&lt;/code&gt; on &lt;code&gt;render()&lt;/code&gt;, but a flattened Canva export is still an opaque bitmap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;toDataURL&lt;/code&gt; is fine for a preview. Download via a Blob.&lt;/strong&gt; MDN now &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL" rel="noopener noreferrer"&gt;warns on &lt;code&gt;toDataURL&lt;/code&gt;&lt;/a&gt;: it builds one giant in-memory string and can overflow URL length limits. Prefer &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob" rel="noopener noreferrer"&gt;&lt;code&gt;toBlob()&lt;/code&gt;&lt;/a&gt; + &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL_static" rel="noopener noreferrer"&gt;&lt;code&gt;URL.createObjectURL&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I put a bunch of these in one place
&lt;/h2&gt;

&lt;p&gt;Once the PDF path worked, the same pattern showed up everywhere: convert in the tab, don't upload, let people pin the tools they actually reuse.&lt;/p&gt;

&lt;p&gt;That's what I shipped as &lt;a href="https://utily.ai" rel="noopener noreferrer"&gt;Utily&lt;/a&gt; — a pile of small file/dev tools that run locally. The one this post is about is &lt;a href="https://utily.ai/pdf-to-png" rel="noopener noreferrer"&gt;PDF to PNG&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I'm not going to list 20 others here. If you only need this conversion, the snippet above is enough to build your own.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would not do
&lt;/h2&gt;

&lt;p&gt;Don't send the &lt;code&gt;ArrayBuffer&lt;/code&gt; to your own API "just to be safe." You just rebuilt the thing you were avoiding.&lt;/p&gt;

&lt;p&gt;Don't promise "unlimited pages at 600 DPI" in a browser. Say the cap. People trust that more.&lt;/p&gt;

&lt;p&gt;If you want the hosted version, it's on Utily. If you want to own it, the loop above is the whole product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://mozilla.github.io/pdf.js/examples/" rel="noopener noreferrer"&gt;PDF.js examples&lt;/a&gt; — &lt;code&gt;getDocument&lt;/code&gt; → &lt;code&gt;getPage&lt;/code&gt; → &lt;code&gt;render&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://mozilla.github.io/pdf.js/api/draft/module-pdfjsLib.html" rel="noopener noreferrer"&gt;PDF.js &lt;code&gt;getDocument&lt;/code&gt; / &lt;code&gt;RenderParameters&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://opensource.adobe.com/dc-acrobat-sdk-docs/library/interapp/IAC_API_CoordinateSystems.html" rel="noopener noreferrer"&gt;Adobe: user space is 1/72 inch&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.itextpdf.com/itext/how-do-the-measurement-systems-in-html-relate-to-t" rel="noopener noreferrer"&gt;ISO 32000 user units, explained&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL" rel="noopener noreferrer"&gt;MDN: prefer &lt;code&gt;toBlob&lt;/code&gt; over &lt;code&gt;toDataURL&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://andergrove.com/posts/are-online-pdf-tools-safe/" rel="noopener noreferrer"&gt;Are online PDF tools safe? (Andergrove)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://privapdf.net/guides/are-online-pdf-tools-safe" rel="noopener noreferrer"&gt;Network-tab test for uploads (PrivaPDF)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

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