<?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: Cetinel</title>
    <description>The latest articles on DEV Community by Cetinel (@getprintoza).</description>
    <link>https://dev.to/getprintoza</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%2F4187126%2F45b4239c-7418-4525-9144-ae28e8044c00.png</url>
      <title>DEV Community: Cetinel</title>
      <link>https://dev.to/getprintoza</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/getprintoza"/>
    <language>en</language>
    <item>
      <title>Why your Puppeteer PDF shows empty boxes for Arabic, Chinese and Hindi (and how to fix it)</title>
      <dc:creator>Cetinel</dc:creator>
      <pubDate>Thu, 15 Oct 2026 16:14:25 +0000</pubDate>
      <link>https://dev.to/getprintoza/why-your-puppeteer-pdf-shows-empty-boxes-for-arabic-chinese-and-hindi-and-how-to-fix-it-299n</link>
      <guid>https://dev.to/getprintoza/why-your-puppeteer-pdf-shows-empty-boxes-for-arabic-chinese-and-hindi-and-how-to-fix-it-299n</guid>
      <description>&lt;p&gt;You generate an invoice PDF from HTML. On your laptop it looks perfect. In production, every Arabic, Chinese or Hindi character is an empty box (□□□). No error, no warning, a perfectly valid PDF. Usually a customer is the first to notice.&lt;/p&gt;

&lt;p&gt;I ran into this enough times that I wrote down what is going on. Nothing is wrong with your code or with Puppeteer.&lt;/p&gt;

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

&lt;p&gt;Here is a script that reproduces it. On a machine with suitable fonts the output looks fine. In a slim container you get boxes:&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;puppeteer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;puppeteer&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;async &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;browser&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;puppeteer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;args&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="s2"&gt;--no-sandbox&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;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;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newPage&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;setContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;html lang="ar" dir="rtl"&amp;gt;&amp;lt;body&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;h1&amp;gt;فاتورة&amp;lt;/h1&amp;gt;&amp;lt;p&amp;gt;你好，世界 · नमस्ते&amp;lt;/p&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/body&amp;gt;&amp;lt;/html&amp;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;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;pdf&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invoice.pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A4&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;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&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;h2&gt;
  
  
  Why it happens
&lt;/h2&gt;

&lt;p&gt;Chrome draws text with the fonts installed on the machine that runs it. Your laptop has hundreds. A minimal Docker image, a CI runner or a serverless function usually has a handful of Latin fonts and nothing else.&lt;/p&gt;

&lt;p&gt;When no installed font contains a character, Chrome draws the "missing glyph" box and moves on. It does not throw. That is why the classic symptom is "works on my machine, broken in production".&lt;/p&gt;

&lt;p&gt;The same is true for Playwright, Selenium or anything else that prints through headless Chrome: the fonts come from the machine, not from the library.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: install Noto fonts in the image
&lt;/h2&gt;

&lt;p&gt;Noto covers Arabic, Hebrew, Devanagari (Hindi) and most other scripts. Chinese, Japanese and Korean live in a separate package. On a Debian-based image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:20-bookworm-slim&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;      fontconfig fonts-noto-core fonts-noto-cjk fonts-noto-color-emoji &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; fc-cache &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rebuild, then check that the system can see a font for each language:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;fc-list :lang&lt;span class="o"&gt;=&lt;/span&gt;ar family | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-3&lt;/span&gt;
fc-list :lang&lt;span class="o"&gt;=&lt;/span&gt;zh-cn family | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-3&lt;/span&gt;
fc-list :lang&lt;span class="o"&gt;=&lt;/span&gt;hi family | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If one of these prints nothing, that language still has no font. Be aware that the CJK package makes the image noticeably bigger, so if size matters, install it only in the image that renders PDFs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fonts are not enough: set &lt;code&gt;lang&lt;/code&gt; and &lt;code&gt;dir&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Two more details decide whether the result looks right, not just not-broken.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Right-to-left text.&lt;/strong&gt; For Arabic and Hebrew, put &lt;code&gt;dir="rtl"&lt;/code&gt; on the &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt; tag (or the container) together with a matching &lt;code&gt;lang&lt;/code&gt;. Chrome handles letter joining and mixed-direction text once a font exists, but without &lt;code&gt;dir="rtl"&lt;/code&gt; alignment, punctuation and numbers can land in the wrong place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chinese vs Japanese.&lt;/strong&gt; The same Han character can be drawn with a different regional shape depending on the language. Characters like 直 and 骨 are good examples. If the page has no &lt;code&gt;lang&lt;/code&gt;, Chrome may pick a font you did not intend, and Chinese text can show up with Japanese shapes. Set &lt;code&gt;lang="zh-CN"&lt;/code&gt;, &lt;code&gt;lang="zh-TW"&lt;/code&gt; or &lt;code&gt;lang="ja"&lt;/code&gt;, and for mixed documents put &lt;code&gt;lang&lt;/code&gt; on the element that holds each piece.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:lang&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;zh-CN&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;"Noto Sans CJK SC"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;sans-serif&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nd"&gt;:lang&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;zh-TW&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;"Noto Sans CJK TC"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;sans-serif&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nd"&gt;:lang&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;ja&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;"Noto Sans CJK JP"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;sans-serif&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;Turkish has its own trap: the dotted and dotless &lt;code&gt;İ&lt;/code&gt; / &lt;code&gt;ı&lt;/code&gt; pair is only upper- and lower-cased correctly when the page declares &lt;code&gt;lang="tr"&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test with a real document
&lt;/h2&gt;

&lt;p&gt;A one-line test hides most problems. Real invoices mix scripts, digits, currency symbols and punctuation. Render one realistic PDF per language in your CI and actually look at it. That is the only way to catch this before a customer does.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you would rather not maintain this
&lt;/h2&gt;

&lt;p&gt;I built a small hosted API around exactly this problem, because I did not want to maintain Chromium and font packages in every project. You POST HTML and get a PDF back, with Noto fonts for 11 languages already installed (en, tr, es, fr, de, ru, ar, he, zh, ja, hi):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://getprintoza.com/v1/convert &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"x-api-key: YOUR_API_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"language": "ar", "html": "&amp;lt;h1&amp;gt;مرحبا&amp;lt;/h1&amp;gt;&amp;lt;p&amp;gt;فاتورة مبيعات&amp;lt;/p&amp;gt;"}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; arabic.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your HTML contains characters that none of the fonts can draw, the PDF is still created and the response carries an &lt;code&gt;X-Getprintoza-Warnings&lt;/code&gt; header listing them. It is a best-effort check, so still look at a few real documents. The free plan is 50 PDFs a month. It is new and run by one person, so I would genuinely like to hear which languages or edge cases still break for you.&lt;/p&gt;

&lt;p&gt;The guides behind this article work with plain Puppeteer and Docker, no API needed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://getprintoza.com/guides/puppeteer-pdf-empty-boxes-arabic-chinese-hindi" rel="noopener noreferrer"&gt;Empty boxes in Puppeteer PDFs (Arabic, Chinese, Hindi)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://getprintoza.com/guides/arabic-hebrew-rtl-pdf-puppeteer-html" rel="noopener noreferrer"&gt;Right-to-left layout for Arabic and Hebrew&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://getprintoza.com/guides/chinese-japanese-pdf-puppeteer-html" rel="noopener noreferrer"&gt;Chinese and Japanese fonts and &lt;code&gt;lang&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://getprintoza.com/guides/turkish-characters-html-to-pdf-fix" rel="noopener noreferrer"&gt;Turkish characters ğ ş ı İ&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>docker</category>
      <category>node</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
