<?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: Abdul Ahad</title>
    <description>The latest articles on DEV Community by Abdul Ahad (@ahadkhan2401).</description>
    <link>https://dev.to/ahadkhan2401</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%2F4055581%2Fc4793bab-d739-4089-952b-18679add0086.png</url>
      <title>DEV Community: Abdul Ahad</title>
      <link>https://dev.to/ahadkhan2401</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ahadkhan2401"/>
    <language>en</language>
    <item>
      <title>Making invoice PDFs in the browser: page breaks, money maths and one trade-off</title>
      <dc:creator>Abdul Ahad</dc:creator>
      <pubDate>Fri, 16 Oct 2026 14:33:58 +0000</pubDate>
      <link>https://dev.to/ahadkhan2401/making-invoice-pdfs-in-the-browser-page-breaks-money-maths-and-one-trade-off-5690</link>
      <guid>https://dev.to/ahadkhan2401/making-invoice-pdfs-in-the-browser-page-breaks-money-maths-and-one-trade-off-5690</guid>
      <description>&lt;p&gt;I run a free invoice generator, &lt;a href="https://www.pdfbillbuilder.com" rel="noopener noreferrer"&gt;PDF Bill Builder&lt;/a&gt;. The PDF is built entirely in the visitor's browser, so nothing is uploaded unless they sign in to save a document. Three problems took longer than I expected. Here is how each one ended up.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Render the preview, then slice it
&lt;/h2&gt;

&lt;p&gt;The design lives in one React component: the live preview. To keep the download identical to the preview, I capture that same DOM with &lt;code&gt;html2canvas-pro&lt;/code&gt; and place the image into a PDF with &lt;code&gt;jsPDF&lt;/code&gt;. The preview has a fixed width (794px for A4, 816px for US Letter), which makes the pixel to millimetre conversion a single division.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pxToMm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pageW&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;offsetWidth&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;contentHpx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;contentHeightMm&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;pxToMm&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trade-off, stated plainly: the result is an image inside a PDF. The text is not selectable. For invoices that people send and print this is usually fine, but if you need selectable text or accessibility, use a text-based PDF library instead. In return I get exact visual parity, any font, any currency symbol, and a logo that just works.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Never cut a row in half
&lt;/h2&gt;

&lt;p&gt;A long invoice needs several pages. Slicing the canvas every &lt;code&gt;pageHeight&lt;/code&gt; pixels cuts table rows through the middle. The fix is to let the preview mark which elements may not be split:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;tr&lt;/span&gt; &lt;span class="na"&gt;data-pdf-block&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; ... &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before slicing, I collect the top and bottom y-position of every marked element, relative to the captured root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;querySelectorAll&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[data-pdf-block]&lt;/span&gt;&lt;span class="dl"&gt;"&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;el&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBoundingClientRect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;ys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&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;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;rootTop&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nx"&gt;ys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&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;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bottom&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;rootTop&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;For each page I pick the largest candidate that is no further than one page height from the start, and no earlier than 35% of the page, so I never produce a nearly empty page. If a single block is taller than a page, I fall back to a hard cut. Page numbers are drawn on multi-page documents.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Money maths: one pure function
&lt;/h2&gt;

&lt;p&gt;Tax added on top versus already included, a percentage or fixed discount, and 18 currencies all multiply the edge cases. I moved everything into one pure function, so the preview and the PDF read the same numbers and cannot disagree.&lt;/p&gt;

&lt;p&gt;Two details mattered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Round every line to two decimals, then sum the rounded lines. That matches what the printed invoice shows.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;toFixed&lt;/code&gt; is not safe for money: &lt;code&gt;1.005.toFixed(2)&lt;/code&gt; gives &lt;code&gt;"1.00"&lt;/code&gt;. I round half up with a small epsilon instead.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;round2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&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="nf"&gt;isFinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&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;sign&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;sign&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;round&lt;/span&gt;&lt;span class="p"&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;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;EPSILON&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&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;Tax "included" is informational only (the total stays the same), while tax "on top" adds to the total. The function is covered by unit tests, which is the cheapest insurance I found against sending a client a wrong total.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell my past self
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Treat the preview as the single source of truth for layout.&lt;/li&gt;
&lt;li&gt;Mark break-safe blocks in the markup instead of guessing in the slicer.&lt;/li&gt;
&lt;li&gt;Keep money maths pure and tested, away from React.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you invoice clients, I would like to hear what is missing. The generator is free with no signup: &lt;a href="https://www.pdfbillbuilder.com" rel="noopener noreferrer"&gt;https://www.pdfbillbuilder.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>typescript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I built a PDF invoice generator that never sends your data to a server</title>
      <dc:creator>Abdul Ahad</dc:creator>
      <pubDate>Thu, 30 Jul 2026 18:52:24 +0000</pubDate>
      <link>https://dev.to/ahadkhan2401/i-built-a-pdf-invoice-generator-that-never-sends-your-data-to-a-server-4l4d</link>
      <guid>https://dev.to/ahadkhan2401/i-built-a-pdf-invoice-generator-that-never-sends-your-data-to-a-server-4l4d</guid>
      <description>&lt;p&gt;Most online invoice tools upload your data. Your client names, your rates, what you earn — it all sits on someone's server. For a tool whose entire job is producing one PDF, that always felt like a bad trade.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://www.pdfbillbuilder.com" rel="noopener noreferrer"&gt;PDF Bill Builder&lt;/a&gt; with a hard rule: nothing leaves the browser.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js 16&lt;/strong&gt; (App Router) + &lt;strong&gt;React 19&lt;/strong&gt; + TypeScript&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;jsPDF + html2canvas&lt;/strong&gt; for PDF generation — both dynamically imported, so they only load when someone actually clicks Download&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailwind CSS v4&lt;/strong&gt; for the marketing pages; the tool itself uses inline styles (deliberate — see below)&lt;/li&gt;
&lt;li&gt;Optional tier: Neon Postgres + Google OAuth + HMAC-signed tokens&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why the tool uses inline styles
&lt;/h2&gt;

&lt;p&gt;The PDF is rendered by rasterising the live preview DOM with html2canvas. Any style that isn't computed at capture time silently disappears from the output. Tailwind's utility classes were fine until a CSS-layer ordering change dropped borders from the PDF but not the screen. Inline styles removed a whole class of bug where the preview and the PDF disagreed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The A4 auto-fit problem
&lt;/h2&gt;

&lt;p&gt;An invoice with 3 line items and one with 30 both need to look right on one page. The preview measures its own rendered height and applies a transform scale so content always fits A4, then html2canvas captures at the scaled size. Simple, and it beat every "paginate manually" approach I tried.&lt;/p&gt;

&lt;h2&gt;
  
  
  What client-side actually buys you
&lt;/h2&gt;

&lt;p&gt;No server means no database of invoices, no breach surface, and no privacy policy gymnastics. It also means no server costs, which is why the free tier can be genuinely unlimited instead of a trial in disguise.&lt;/p&gt;

&lt;p&gt;The trade-off: no cross-device sync unless you sign in. That felt like the right default for a tool people use once a month.&lt;/p&gt;

&lt;p&gt;Code: &lt;a href="https://github.com/Ahad-Khan2401/invoice-generator" rel="noopener noreferrer"&gt;https://github.com/Ahad-Khan2401/invoice-generator&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy to answer questions about the jsPDF pipeline — it was the fiddliest part by far.&lt;/p&gt;

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