<?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: Monkee Tools</title>
    <description>The latest articles on DEV Community by Monkee Tools (@monkeetools).</description>
    <link>https://dev.to/monkeetools</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%2F4124765%2F64313be3-2e74-4cd8-838c-17ee74a22c25.png</url>
      <title>DEV Community: Monkee Tools</title>
      <link>https://dev.to/monkeetools</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/monkeetools"/>
    <language>en</language>
    <item>
      <title>42 free tools that work without an account (hand-checked)</title>
      <dc:creator>Monkee Tools</dc:creator>
      <pubDate>Mon, 14 Sep 2026 20:41:22 +0000</pubDate>
      <link>https://dev.to/monkeetools/42-free-tools-that-work-without-an-account-hand-checked-2gni</link>
      <guid>https://dev.to/monkeetools/42-free-tools-that-work-without-an-account-hand-checked-2gni</guid>
      <description>&lt;p&gt;I run a free invoice generator for freelancers, and the whole product rests on one belief: you should not need an account to do a simple job. That belief turned into a side project: a hand-checked list of free tools that do their core job with no signup, no login, and no "free trial" that is really a signup wall with better marketing.&lt;/p&gt;

&lt;p&gt;42 tools, 12 categories. I checked every one against its own site. Here is the full list, with my pick in each category marked.&lt;/p&gt;

&lt;p&gt;The bar for getting listed (all five must be true):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Free core use. Paid extras are fine, a paywall on the main feature is not.&lt;/li&gt;
&lt;li&gt;No account for the main feature. A free trial is a signup wall with better marketing.&lt;/li&gt;
&lt;li&gt;Freelancer-relevant. If a freelancer would not use it weekly, it does not belong.&lt;/li&gt;
&lt;li&gt;No dark patterns. No fake countdowns, no email hostage-taking, no "download our app to continue."&lt;/li&gt;
&lt;li&gt;Checked by hand. Every listing verified against the tool's own site before it goes live.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Get paid&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MonkeeTools Invoice Generator (ours): free invoices that remember you, everything stays in your browser.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Time tracking&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Konverter Time Tracker (our pick): browser timer with your hourly rate and currency ticking live, all client-side.&lt;/li&gt;
&lt;li&gt;Pomofocus: customizable Pomodoro timer for focused sessions, the core timer needs no account.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Contracts and e-signing&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PactSign (our pick): generates freelance agreements, retainers, and NDAs from a short form, 100% in your browser.&lt;/li&gt;
&lt;li&gt;eSign: send PDFs or Word docs for electronic signature, plus free freelance contract templates.&lt;/li&gt;
&lt;li&gt;Docracy: free e-signing, documents delete themselves after 9 days.&lt;/li&gt;
&lt;li&gt;Smallpdf eSign: draw, type, or upload a signature onto a PDF in the browser.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;File sharing&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SwissTransfer: send up to 50 GB free, stored in Switzerland, links valid 30 days.&lt;/li&gt;
&lt;li&gt;Wormhole (our pick): end-to-end encrypted transfer up to 10 GB, files encrypted in your browser before upload.&lt;/li&gt;
&lt;li&gt;WeTransfer: up to 2 GB free by link or email, files auto-delete after 7 days.&lt;/li&gt;
&lt;li&gt;file.io: anonymous uploads, the link self-destructs after the first download.&lt;/li&gt;
&lt;li&gt;FilePizza: peer-to-peer sending straight from the browser, nothing stored on a server.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;PDF tools&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PDF24 Tools (our pick): merge, split, compress, convert, watermark, and sign. The widest free set, no account.&lt;/li&gt;
&lt;li&gt;Sejda: edit, merge, compress, fill, and sign; files deleted from their servers after processing.&lt;/li&gt;
&lt;li&gt;iLovePDF: merge, compress, convert, and edit, the core tools work as a guest.&lt;/li&gt;
&lt;li&gt;Convertio: convert between 300+ formats on the free tier, no account.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Images&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Squoosh: shrink images without uploading them, the codecs run in your browser. (We use it: our logo and og-image went through it.)&lt;/li&gt;
&lt;li&gt;QRCode Monkey: custom QR codes with logos and vector downloads.&lt;/li&gt;
&lt;li&gt;Coolors: one-press color palette generator for client brand work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Meetings&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whocan: availability polls with RSVP, email reminders, and anonymous voting, no registration.&lt;/li&gt;
&lt;li&gt;Jitsi Meet (our pick): free open-source video calls in the browser, no account, no download.&lt;/li&gt;
&lt;li&gt;WorldTimeBuddy: visual time-zone converter for finding meeting times across client time zones.&lt;/li&gt;
&lt;li&gt;When2meet: group availability polls where nobody needs an account, organizer included.&lt;/li&gt;
&lt;li&gt;Framadate: open-source scheduling polls from a nonprofit, no ads, no tracking.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Secret sharing&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OneTimeSecret: password and credential links that self-destruct after one view.&lt;/li&gt;
&lt;li&gt;Yopass (our pick): open-source, end-to-end encrypted one-time secret links.&lt;/li&gt;
&lt;li&gt;PrivNote: self-destructing encrypted notes with optional password and expiry.&lt;/li&gt;
&lt;li&gt;Password Pusher: secret links with expiry and view limits, running since 2011.&lt;/li&gt;
&lt;li&gt;1Password Strong Password Generator: instant random, memorable, or PIN passwords in the browser, no gate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Money math&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Xe: currency converter with live mid-market rates across 100+ currencies.&lt;/li&gt;
&lt;li&gt;Omni Calculator (our pick): the finance calculators freelancers actually need: VAT, sales tax, salary-to-hourly, margins.&lt;/li&gt;
&lt;li&gt;EconKit Rate Calculator: computes a sustainable hourly rate from target income, overhead, and taxes, in your browser.&lt;/li&gt;
&lt;li&gt;Toggl Billable Hours Calculator: turns hours worked into billable revenue, no account.&lt;/li&gt;
&lt;li&gt;Plutio Tax Calculator: freelance quarterly tax estimates with take-home and set-aside amounts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Portfolio&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PortfolioForge (our pick): fill in a form, download your complete portfolio site as a single HTML file.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Sharper proposals&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hemingway Editor: flags dense sentences, passive voice, and hard-to-read phrases, free web editor.&lt;/li&gt;
&lt;li&gt;LanguageTool (our pick): grammar, spelling, and style checking in 30+ languages.&lt;/li&gt;
&lt;li&gt;TinyURL: shorten long client links, no account.&lt;/li&gt;
&lt;li&gt;Dillinger: browser markdown editor with live preview and one-click PDF or HTML export.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Video and voice&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Online Video Cutter: trim, crop, and rotate video clips in the browser.&lt;/li&gt;
&lt;li&gt;Online Voice Recorder: record voice memos and call notes as MP3, everything stays on your device.&lt;/li&gt;
&lt;li&gt;SpeechTexter: free real-time dictation for drafting client emails hands-free.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Worth the signup (the honest exception shelf)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The wall above is strictly no-login. Two tools need an account, and I list them anyway because I use them to run my own site and vouch for them personally. Both have free tiers I actually use; I have never paid either of them a cent.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;UptimeRobot: 50 free uptime monitors, watches my site around the clock.&lt;/li&gt;
&lt;li&gt;Airtable: spreadsheet with superpowers, tracks my maker outreach on the free tier.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;The full list lives here with search and filters: &lt;a href="https://monkeetools.surge.sh/no-login-tools/" rel="noopener noreferrer"&gt;https://monkeetools.surge.sh/no-login-tools/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Built a free tool that works without an account? There is a submission form on the page. No account needed to submit, obviously. I review every submission by hand.&lt;/p&gt;

&lt;p&gt;And the disclosure: the "get paid" slot is my own MonkeeTools invoice generator. One rule on the wall: I do not list other invoice generators. That is my thing.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>productivity</category>
      <category>freelance</category>
    </item>
    <item>
      <title>I hand-rolled a PDF engine in vanilla JS so our invoice app needs zero dependencies</title>
      <dc:creator>Monkee Tools</dc:creator>
      <pubDate>Mon, 14 Sep 2026 15:16:07 +0000</pubDate>
      <link>https://dev.to/monkeetools/i-hand-rolled-a-pdf-engine-in-vanilla-js-so-our-invoice-app-needs-zero-dependencies-3mii</link>
      <guid>https://dev.to/monkeetools/i-hand-rolled-a-pdf-engine-in-vanilla-js-so-our-invoice-app-needs-zero-dependencies-3mii</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a build-in-public deep dive from &lt;a href="https://monkeetools.pages.dev/" rel="noopener noreferrer"&gt;MonkeeTools&lt;/a&gt;, a free invoice generator that runs entirely in your browser. No account, no server, no tracking. This post is about the weirdest part of that promise: the PDF engine, written by hand, with no libraries.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why would anyone do this
&lt;/h2&gt;

&lt;p&gt;Most "download as PDF" buttons in web apps work one of two ways. Either the browser's print-to-PDF does the job (inconsistent, ugly, zero control over layout), or a library like jsPDF or pdf-lib builds the document. Those libraries are fine. They are also 300KB+ of dependency surface for what is, at its core, a pretty simple file format.&lt;/p&gt;

&lt;p&gt;We had a harder constraint than most: our entire product thesis is "nothing leaves your browser." No backend, no npm install, no build step. The site is static HTML, CSS, and vanilla JS on a static host. Pulling in a PDF library would not violate the privacy story, but it would violate the spirit: a single self-contained page you could save and run offline. So I wrote the PDF generator by hand. Every byte.&lt;/p&gt;

&lt;p&gt;It is about 130 lines of actual PDF assembly, plus helpers. Let me show you how it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  A PDF is just numbered objects and a phone book
&lt;/h2&gt;

&lt;p&gt;Strip away the mystique and a PDF is: a header, a list of numbered objects, a cross-reference table (the "phone book" that says object N lives at byte offset X), and a trailer that points at the phone book. That is the whole format, at least for the simple documents we generate.&lt;/p&gt;

&lt;p&gt;Our &lt;code&gt;buildPDF()&lt;/code&gt; function assembles the file in three phases. First, small helper closures turn invoice data into PDF content-stream commands:&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;cmds&lt;/span&gt; &lt;span class="o"&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;txt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&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="nx"&gt;size&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;bold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;color&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0.09 0.14 0.17&lt;/span&gt;&lt;span class="dl"&gt;'&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;cmds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`BT /F&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;bold&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; Tf &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; rg &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &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="s2"&gt; Td (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;pdfStr&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="s2"&gt;) Tj ET`&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;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;x2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y2&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;=&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="nx"&gt;color&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.82 .85 .83&lt;/span&gt;&lt;span class="dl"&gt;'&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;cmds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; RG &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; w &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;x1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;y1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; m &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;x2&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;y2&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; l S`&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;txt()&lt;/code&gt; emits a text-drawing operation (begin text, select font, set color, move, show string, end text). &lt;code&gt;line()&lt;/code&gt; strokes a rule. The template renderers push hundreds of these into &lt;code&gt;cmds&lt;/code&gt;, and the content stream becomes one PDF object.&lt;/p&gt;

&lt;p&gt;Second, the objects themselves: catalog, page tree, page, content stream, two fonts, font descriptors, font files, and annotations. Each is serialized with its byte offset recorded:&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;N&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;logoImg&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="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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payURL&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="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;The object count is computed dynamically because the logo image and the Pay now link annotation are optional. Get this number wrong and the file is corrupt, so it is derived, never hardcoded.&lt;/p&gt;

&lt;p&gt;Third, the xref table and trailer, built from the recorded offsets:&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;let&lt;/span&gt; &lt;span class="nx"&gt;trailer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;xref&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;0 &lt;/span&gt;&lt;span class="dl"&gt;'&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;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;0000000000 65535 f &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&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;N&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="nx"&gt;trailer&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;offsets&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="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; 00000 n &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;trailer&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;trailer&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;&amp;lt; /Size &lt;/span&gt;&lt;span class="dl"&gt;'&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;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; /Root 1 0 R &amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;startxref&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;xref&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;%%EOF&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 is the entire file format. Header, objects, phone book, done. The first time a real PDF reader opened the output without complaining, I celebrated more than I should admit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fonts: subsetted Inter with WinAnsiEncoding
&lt;/h2&gt;

&lt;p&gt;Text in a PDF needs a font, and we wanted our brand font (Inter) to render identically everywhere, not fall back to Helvetica on systems that lack it. So we embed two subsetted Inter files, regular and bold, as FontFile2 streams, with a hand-written font descriptor each:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;&amp;lt; /Type /Font /Subtype /TrueType /BaseFont /Inter-Regular
   /FirstChar 32 /LastChar 255 /Widths 8 0 R
   /Encoding /WinAnsiEncoding /FontDescriptor 9 0 R &amp;gt;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;WinAnsiEncoding is the honest tradeoff at the heart of this engine. It covers Latin-1 plus a slice of typographic characters, which handles every Western European language, but it cannot do emoji, CJK, Arabic, or Cyrillic. Rather than fail silently, our &lt;code&gt;pdfStr()&lt;/code&gt; escaper handles the boundary explicitly:&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;WINANSI&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;€&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;…&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;133&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;‘&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;145&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;’&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;146&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;“&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;147&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;”&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;148&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;–&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;—&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;151&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;™&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;153&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;pdfStr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&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;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&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;ch&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&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;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;codePointAt&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;127&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;out&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ch&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;ch&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;ch&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;ch&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&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;c&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;out&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;padStart&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&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;WINANSI&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ch&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="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;padStart&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Last resort: strip diacritics, else '?'. Never emit a broken byte.&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NFKD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[^\x&lt;/span&gt;&lt;span class="sr"&gt;20-&lt;/span&gt;&lt;span class="se"&gt;\x&lt;/span&gt;&lt;span class="sr"&gt;7E&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;out&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;?&lt;/span&gt;&lt;span class="dl"&gt;'&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;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;out&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;ASCII passes through (with PDF string escapes for backslash and parens), Latin-1 becomes octal escapes, known typographic characters map through the WINANSI table, and anything else gets NFKD-normalized so "é" degrades to "e" rather than corrupting the file. A "?" is ugly, but a corrupt PDF is worse. This is the kind of decision a library hides from you; hand-rolling forces you to make it consciously.&lt;/p&gt;

&lt;h2&gt;
  
  
  Clickable links need geometry, not just text
&lt;/h2&gt;

&lt;p&gt;A PDF link is not part of the text. It is an annotation: a rectangle on the page plus an action. The critical detail is that the rectangle must match the rendered text, or users click empty space. Our footer tagline is a link back to the site, and its annotation rect is measured against the actual laid-out text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;&amp;lt; /Type /Annot /Subtype /Link /Rect [160 27 435 41]
   /Border [0 0 0]
   /A &amp;lt;&amp;lt; /S /URI /URI (https://monkeetools.surge.sh/) &amp;gt;&amp;gt; &amp;gt;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Pay now button is fancier: the button is drawn as a filled rectangle with text, and the annotation rect is computed from the button geometry with a small padding margin, so the whole button is clickable:&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;payBtn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pbx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pby&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fg&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;pbw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;104&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pbh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;cmds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; rg &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;pbx&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;pby&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;pbw&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;pbh&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; re f`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;txt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pbx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pby&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Pay now&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;&amp;lt; /Type /Annot /Subtype /Link `&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="s2"&gt;`/Rect [&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;pbx&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;pby&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;pbx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;pbw&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;pby&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;pbh&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;] `&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="s2"&gt;`/Border [0 0 0] /A &amp;lt;&amp;lt; /S /URI /URI (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;pdfStr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payURL&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;) &amp;gt;&amp;gt; &amp;gt;&amp;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;One annotation object per link, referenced in the page's &lt;code&gt;/Annots&lt;/code&gt; array. The object count math from earlier has to account for these, which is why N is computed, not constant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three templates, one core
&lt;/h2&gt;

&lt;p&gt;The invoice comes in three designs (Classic, Minimal, Modern), but there is exactly one &lt;code&gt;buildPDF()&lt;/code&gt;. The shared core handles data, fonts, xref, annotations, and the logo; the templates are just three renderer functions that push different drawing commands into the same &lt;code&gt;cmds&lt;/code&gt; array:&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;payAnnot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tpl&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;minimal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;drawMinimal&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
               &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tpl&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;modern&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;  &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;drawModern&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
               &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;drawClassic&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This separation fell out naturally from hand-rolling: when you own the content stream, "a different template" just means "different commands in the array." No template engine, no layout framework. The preview on screen and the PDF share the same data model, so what you see is what the bytes say.&lt;/p&gt;

&lt;h2&gt;
  
  
  Logos: from file picker to DCTDecode stream
&lt;/h2&gt;

&lt;p&gt;Users can upload a logo, which travels a fun path: &lt;code&gt;FileReader.readAsDataURL()&lt;/code&gt; gives us a data URL, we decode the base64 back to bytes, and if it is a JPEG those bytes go straight into the PDF as an image XObject with DCTDecode (JPEG) compression, no re-encoding, no quality loss:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;&amp;lt; /Type /XObject /Subtype /Image /Width 480 /Height 120
   /ColorSpace /DeviceRGB /BitsPerComponent 8
   /Filter /DCTDecode /Length 24576 &amp;gt;&amp;gt;
stream
...raw JPEG bytes...
endstream
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is drawn with a transformation matrix that scales it to fit the header box while preserving aspect ratio. The neat part: since JPEG is already the PDF's native compressed image format, embedding is essentially zero-copy. The bytes the user uploaded are the bytes in the PDF.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest tradeoffs
&lt;/h2&gt;

&lt;p&gt;Build-in-public means showing the rough edges, so here are ours:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verified on Poppler only.&lt;/strong&gt; Our claim is that the PDFs render correctly, and what we have actually verified is Poppler (the open-source renderer). Acrobat, macOS Preview, and mobile readers are untested from our Linux environment. The PDFs use only baseline PDF 1.4 features, so breakage is unlikely, but "unlikely" is not "verified," and we say so on the site.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WinAnsi, not Unicode.&lt;/strong&gt; As shown above, anything outside Latin-1 plus our typographic table degrades. For an invoicing tool aimed at freelancers in Western markets this is a fine trade today, but full Unicode (ToUnicode CMaps, embedded CID fonts) is the obvious future work if we ever need it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Single page, fixed layout.&lt;/strong&gt; We cap invoices at 12 line items, which fits one A4 page at our layout metrics. Multi-page support means page-breaking logic we have not needed yet. The cap is enforced in the UI, so the engine never has to handle overflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No text reflow.&lt;/strong&gt; Long descriptions do not wrap; they are clipped by layout. Again, a UI-level constraint (field lengths) keeps the engine simple.&lt;/p&gt;

&lt;p&gt;Every one of these is a conscious scope decision, not an accident. The engine does exactly what the product needs and nothing more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just use a library
&lt;/h2&gt;

&lt;p&gt;Honestly? For most projects, use pdf-lib. It is excellent. We hand-rolled because our constraints were unusual: zero dependencies, zero build step, a single HTML file that works offline, and a privacy story that is easier to tell when there is literally no third-party code. The engine is ~130 lines of assembly plus helpers, and every behavior in it is one we chose deliberately.&lt;/p&gt;

&lt;p&gt;There is also a less rational reason: writing a PDF by hand teaches you what a PDF actually is, and that understanding paid for itself ten times over in debugging. When a user reports a rendering quirk, I do not file an issue against a library. I open the bytes.&lt;/p&gt;

&lt;p&gt;If you want to see the output, the generator is free and needs no account: &lt;a href="https://monkeetools.pages.dev/" rel="noopener noreferrer"&gt;monkeetools.pages.dev&lt;/a&gt;. Make an invoice, download the PDF, and know that every byte of it was placed there on purpose.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;MonkeeTools is a free invoice generator that remembers you without an account. Everything runs in your browser, including the PDF engine described here. Built in the open, one changelog entry at a time.&lt;/em&gt;&lt;/p&gt;

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