DEV Community

Aleksandrov Todor
Aleksandrov Todor

Posted on

How I fixed Cyrillic support in jsPDF (the clean way)

If you've ever tried to generate a PDF with jsPDF that contains Cyrillic (or really any non-Latin text), you've probably seen this: instead of your text, you get garbage characters, question marks, or nothing at all.

I hit this exact wall while building an invoicing app that needed to output quotes in both English and Bulgarian. Here's how I solved it properly.

Why it happens

jsPDF ships with the standard PDF fonts (Helvetica, Times, Courier). These fonts simply don't contain Cyrillic glyphs. So when you ask jsPDF to render "Оферта", it has nothing to draw.

The common "fix" you'll see online is transliteration — converting "Оферта" to "Oferta". But that's not a fix. Your Bulgarian client gets a document in mangled Latin letters, and it looks unprofessional.

The real solution: embed a Unicode font

The proper way is to embed a font that actually contains Cyrillic glyphs. I used DejaVu Sans — it's free, open source, and covers Latin + Cyrillic in one file.

The steps:

  1. Convert the .ttf font file to a base64 string
  2. Register it with jsPDF using addFileToVFS and addFont
  3. Set it as the active font before writing text

Here's the registration part:

import { jsPDF } from "jspdf";

export function registerCyrillicFont(doc) {
  doc.addFileToVFS("DejaVuSans.ttf", DejaVuSansBase64);
  doc.addFont("DejaVuSans.ttf", "DejaVuSans", "normal");
  doc.addFileToVFS("DejaVuSans-Bold.ttf", DejaVuSansBoldBase64);
  doc.addFont("DejaVuSans-Bold.ttf", "DejaVuSans", "bold");
}
Enter fullscreen mode Exit fullscreen mode

Then before writing any text:

const doc = new jsPDF();
registerCyrillicFont(doc);
doc.setFont("DejaVuSans", "normal");
doc.text("Оферта за строителни материали", 20, 20); // renders perfectly
Enter fullscreen mode Exit fullscreen mode

Converting the font to base64

You can do this in a few lines of Python:

import base64
with open("DejaVuSans.ttf", "rb") as f:
    encoded = base64.b64encode(f.read()).decode("ascii")
# save `encoded` into a .js file as a string constant
Enter fullscreen mode Exit fullscreen mode

The base64 string is large (~1MB for the regular weight), so keep it in a separate file and import it — don't inline it in your main component.

A gotcha: bold weight

If you use doc.setFont("DejaVuSans", "bold") but only registered the regular weight, jsPDF silently falls back to a font that has no Cyrillic — and you're back to broken text. Register both the regular and bold .ttf files, as shown above.

The result

Clean, professional PDFs in both languages, from the same codebase — no transliteration, no broken characters.

I actually built this into a full invoicing & quote app (React + Capacitor). If you want to see the whole thing in action, there's a demo here: https://youtu.be/uxR4yrXYjV8

And the full source code is available if you'd rather not build invoicing from scratch: https://antovtodor1.gumroad.com/l/quote-studio

Hope this saves someone the hours I spent figuring it out. Happy to answer questions in the comments!

javascript, react, webdev, tutorial

Top comments (1)

Collapse
 
nube_colectiva_nc profile image
Nube Colectiva

Great tutorial, thanks for sharing 👍🏼