How We Generate PDF Invoices That Don't Break on Windows 7

Short answer: I thought PDF was a solved problem. It isn't.

Compatibility note: LockMargin requires Windows 10 or later. This guide covers PDF generation on Windows 7 machines in a legacy Tauri/IE11 context.

The Problem: Why PDF Breaks Where You Can't See It

I started with the obvious. Tauri has a window.print() API. HTML + CSS > WebView > PDF. Works on my Mac? Works. On Windows 10? Works. On Windows 7?

No.

Windows 7 doesn't support WebView2 (Edge Chromium). It supports EdgeHTML — the engine from Windows 10, if updates are installed. If not — IE11. IE11. In 2026. My client uses IE11 to render PDFs because Tauri print-to-PDF uses the system WebView.

What breaks:

I spent a week on forums. Tauri Discord, Rust Reddit, Typst community, Stack Overflow tagged internet-explorer-11 (yes, it's still alive). Different people, different advice.

"Forget it, Windows 7 is dead, don't support it." — I replied: "My client isn't dead. His computer isn't either."

"Use @react-pdf/renderer, renders in JS, no WebView dependency." — I looked. React. In my Rust app. WASM integration. Bundle size +15 MB. No.

"Headless Chrome in Docker." — Docker. In a desktop app. No.

"Just use wkhtmltopdf." — wkhtmltopdf uses QtWebKit. Which on Windows 7... the same WebKit as old Safari. Which doesn't support modern CSS. Full circle.

I was stuck. Then I found Typst.

The Solution: Typst, or "Why Didn't I Find This on Day Three"

Typst is a Rust document compiler. Doesn't render HTML to PDF. Compiles templates to PDF directly. No WebView dependency. No system fonts dependency. No OS-specific dependency — compiles identically everywhere.

I rewrote PDF generation in two days. Typst template:

#let invoice(data) = { set document( title: "Invoice " + data.number, author: "LockMargin" ) set page( paper: "a4", margin: (top: 2cm, bottom: 2cm, left: 2.5cm, right: 2.5cm) ) set text( font: "Noto Sans", size: 10pt, lang: data.language ) // RTL support if data.language == "ar" { set text(dir: rtl) } // Header align(right)[#image("logo.svg", width: 3cm)] v(1cm) // Invoice details grid( columns: (1fr, 1fr), gutter: 1cm, [ *From:* \ #data.from.name \ #data.from.address ], align(right)[ *To:* \ #data.to.name \ #data.to.address ] ) // Items table table( columns: (3fr, 1fr, 1fr, 1fr), [*Description*], [*Qty*], [*Rate*], [*Amount*], ..data.items.map(item => ( item.description, str(item.quantity), str(item.rate), str(item.amount) )).flatten() ) // Totals align(right)[ *Subtotal:* #data.subtotal \ *Tax:* #data.tax \ *Total:* #data.total ]}

Simple? Simple. But fonts.

Fonts: 20 MB of Hatred

Typst needs fonts. Not system fonts — embedded in PDF. I used Noto Sans. For Latin — 400 KB. For Latin + Cyrillic — 800 KB. For Latin + Cyrillic + Arabic (Noto Sans Arabic) — 1.2 MB. For CJK (Noto Sans CJK) — 18 MB.

Eighteen megabytes. One font. My invoice — 50 KB of text + 18 MB of font. Client in Saudi Arabia gets an email: "Your invoice, 18 MB." He writes: "Is this a virus?"

The fix — subsetting. Typst supports subsetting out of the box. I compile the template, Typst analyzes which glyphs are needed, and includes only those. Arabic invoice — 1.4 MB instead of 18. English — 420 KB.

But compilation... On my Dell Inspiron "workhorse" — 4 minutes. On the desktop "Fujik" — 45 seconds. First time I thought it froze. Went to make tea. Came back — done. Now I know: Typst compilation = tea time.

RTL, CJK, and Other Monsters

The Arabic client wasn't the only one. What if someone needs Japanese fonts? CJK fonts, vertical text (not supported yet, but we checked), kanji. Typst + Noto Sans CJK = works.

RTL — Arabic, Hebrew. Typst supports dir: rtl at block level. But numbers in Arabic text stay LTR. This is correct — Arabic numerals are written left-to-right even in RTL context. But my tables... Tables with numbers in RTL context — a separate nightmare. Columns mirror. "Quantity" on the right, "Price" on the left, but numbers inside — LTR. I spent three hours drawing diagrams on paper. Then realized: columns: (1fr, 1fr, 1fr, 1fr) in RTL — mirrors automatically. Nothing to do. Typst already thought of this.

CJK — Chinese, Japanese, Korean. Noto Sans CJK — 18 MB full, 2.3 MB after subsetting for a typical invoice. But there's a problem: a Japanese client wants ??? (vertical text). Typst doesn't support vertical text out of the box. I said: "Horizontal for now. Vertical — in 2.0." They agreed. I exhaled with relief.

PDF/A: Archiving Nobody Asked For

My German client's accountant wrote: "We need PDF/A-1b for the tax office." I didn't know what PDF/A was. Turns out — archival PDF standard. Bans JavaScript, requires embedded fonts, requires metadata. Typst generates PDF/A-1b out of the box. I didn't know. Just checked — and it was there. Like in "The Shawshank Redemption": "Is it worth it?" — "Yes, it is."

File Size: Freelancers Email Their Invoices

My first Typst PDF — 420 KB. One-page invoice, SVG logo, Noto Sans subset. Client in Saudi Arabia opened it on Windows 7, Acrobat Reader 2015. Wrote: "Works. Thanks."

I sat and stared at the screen for ten minutes. One word. "Works." A week of forums, three liters of tea, loud noise outside, spilled tea on the keyboard — and one word.

Then he wrote: "But the logo is slightly blurry." I closed the laptop and went to sleep. Next day I figured it out — SVG logo, Acrobat 2015 renders SVG through an old engine. Converted the logo to PDF and embedded it as XObject. Became sharp. He wrote: "Perfect."

The Lesson: Three Questions for Any PDF Generator

Before you trust an app with your invoices:

  1. What rendering engine? If "system WebView" — run. Windows 7, corporate policies, old Acrobat — it will all break.
  2. Fonts embedded or system? If system — your PDF will look different on every machine. Embed. Subsetting is mandatory.
  3. RTL and CJK supported? If not — you're losing clients. The world is bigger than Latin.

I'm not saying LockMargin generates perfect PDFs. I'm saying we think about machines most developers consider dead. Windows 7 dead? Tell that to my client. His computer works. His business works. My PDF now works too.

FAQ

Why not Tauri print-to-PDF?

Tauri print-to-PDF uses the system WebView. On Windows 7 this is EdgeHTML or IE11, which don't support modern CSS, emoji, and have bugs with print media queries.

Why Typst and not @react-pdf/renderer?

@react-pdf/renderer is a JS library, requires React +15 MB to the app. Typst is a Rust compiler, integrates natively, doesn't depend on WebView.

How does font subsetting work?

Typst analyzes which glyphs are used in the document and includes only those in the PDF. An Arabic invoice is 1.4 MB instead of 18 MB for the full Noto Sans CJK.

Is PDF/A supported?

Yes, Typst generates PDF/A-1b out of the box — required for tax archiving in some jurisdictions.

How do RTL tables work?

Typst supports dir: rtl at block level. Tables mirror automatically, numbers stay LTR. No manual column configuration needed.

P.S. — I've been in this state for a while now: when it's quiet, I can't work — something feels off. The client from Saudi Arabia sent a second order. I asked about Windows 7. He replied: "We upgraded to Windows 10. But the PDF still has to work." I agreed. Didn't tell him it now works everywhere — not just on Windows 7. Didn't want to risk it.

Ready to own your freelance data?

Standard is $49 one-time — no subscriptions, no cloud, no data mining. Your data stays on your machine, encrypted with AES-256-GCM.

Download Free — No Account Needed Compare Pricing

What's Next

Post #7: How We Built a Backup System Nobody Asked For — or how I realized freelancers lose data more often than they get hacked.

Continue reading the Building LockMargin series.

Vlad (Volodymyr) Shiyan, founder of LockMargin

About the Author

Vlad (Volodymyr) Shiyan — Founder & Developer, Kharkiv, Ukraine. Building LockMargin since December 2025. Offline-first invoicing for freelancers who are tired of subscriptions. Standard is $49 one-time. Read more about Vlad →

Get one practical guide each month on building a business you own

No spam. No fluff. Unsubscribe anytime.