<?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: Md Risfat</title>
    <description>The latest articles on DEV Community by Md Risfat (@risfat).</description>
    <link>https://dev.to/risfat</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%2F589500%2F24049c49-0ae2-4475-9c77-2f58932e3fe1.jpeg</url>
      <title>DEV Community: Md Risfat</title>
      <link>https://dev.to/risfat</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/risfat"/>
    <language>en</language>
    <item>
      <title>Why Bangla Text Breaks in PDFs (Even Chrome's Own PDFs Lose Up to 77% of the Words)</title>
      <dc:creator>Md Risfat</dc:creator>
      <pubDate>Thu, 05 Nov 2026 08:46:05 +0000</pubDate>
      <link>https://dev.to/risfat/why-bangla-text-breaks-in-pdfs-even-chromes-own-pdfs-lose-up-to-77-of-the-words-1b94</link>
      <guid>https://dev.to/risfat/why-bangla-text-breaks-in-pdfs-even-chromes-own-pdfs-lose-up-to-77-of-the-words-1b94</guid>
      <description>&lt;p&gt;Read this word:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;অিভজ্ঞতা&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you read Bangla, your eyes stumbled for a moment. The vowel sign ি is on the wrong side of ভ. It should say &lt;strong&gt;অভিজ্ঞতা&lt;/strong&gt;: &lt;em&gt;experience&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;You recovered in half a second. Software doesn't. To a program, অিভজ্ঞতা isn't a misspelling of "experience". It's a word that doesn't exist.&lt;/p&gt;

&lt;p&gt;That word came out of a real CV, uploaded to the &lt;a href="https://promptcv.app/ats-checker" rel="noopener noreferrer"&gt;ATS checker&lt;/a&gt; we built at &lt;a href="https://promptcv.app" rel="noopener noreferrer"&gt;PromptCV&lt;/a&gt;, the CV maker I build for job seekers in Bangladesh. The CV clearly had an Experience section. Our checker told the person it didn't, because every Bangla heading had come out of the PDF scrambled like this.&lt;/p&gt;

&lt;p&gt;So we fixed the checker. Then, to measure the fix, we also ran &lt;strong&gt;our own&lt;/strong&gt; exported PDFs through it.&lt;/p&gt;

&lt;p&gt;They failed too.&lt;/p&gt;

&lt;p&gt;The culprit turned out to be Chrome. In the PDFs it printed for us, PDF readers were losing up to 77% of the Bangla words. This post is about three different ways Bangla text breaks in PDFs, how to detect each one in JavaScript, and how we repaired Chrome's output without changing a single pixel.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Bangla is broken in my PDF" is really &lt;strong&gt;three bugs&lt;/strong&gt;: legacy Bijoy fonts, text stored in visual order, and the way Chrome writes complex scripts into PDFs.&lt;/li&gt;
&lt;li&gt;Two of them can be detected with near-certainty using one regex and a character-category trick (code below).&lt;/li&gt;
&lt;li&gt;Chrome/Puppeteer PDFs look perfect and still extract badly. Across 9 PDF readers, the words recovered dropped as low as 23%. We rebuilt the text layer from data Chrome already embeds, and now all 9 get 100%.&lt;/li&gt;
&lt;li&gt;Check your own PDF in one line: &lt;code&gt;pdftotext your.pdf - | grep -nP '(^|\s)[িেৈ]'&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Three bugs that look like one
&lt;/h2&gt;

&lt;p&gt;Copy Bangla out of a PDF and you'll usually get one of these:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Avwg GKRb †mvdUIq¨vi BwÄwbqvi|      ← should be: আমি একজন সফটওয়্যার ইঞ্জিনিয়ার।
িহসাবরক্ষক  অিভজ্ঞতা                  ← should be: হিসাবরক্ষক অভিজ্ঞতা
বর্ত মান  রা কি ব                      ← words split, letters missing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They look like the same problem. They aren't:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you see&lt;/th&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;th&gt;Where it comes from&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Latin junk like &lt;code&gt;Avwg GKRb&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Legacy (non-Unicode) fonts: Bijoy, SutonnyMJ&lt;/td&gt;
&lt;td&gt;Documents typed in Bijoy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vowel signs before the consonant: &lt;code&gt;িহসাব&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Text stored in &lt;strong&gt;visual&lt;/strong&gt; order&lt;/td&gt;
&lt;td&gt;Many PDF producers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Split words, missing letters, invisible NUL characters&lt;/td&gt;
&lt;td&gt;How Chrome writes complex scripts into PDFs&lt;/td&gt;
&lt;td&gt;Anything printed from Chrome or Puppeteer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Let's take them one at a time, from the most familiar to the most surprising.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 1: The ghost of Bijoy
&lt;/h2&gt;

&lt;p&gt;If you've grown up in Bangladesh, you've seen &lt;code&gt;Avwg GKRb&lt;/code&gt; before. It's Bangla typed with the Bijoy keyboard in a font like SutonnyMJ, and it was everywhere before Unicode Bangla took over.&lt;/p&gt;

&lt;p&gt;The trick behind it is a costume. These fonts don't store Bangla at all. They store plain Latin letters and &lt;em&gt;draw&lt;/em&gt; Bangla shapes on top: &lt;code&gt;A&lt;/code&gt; + &lt;code&gt;v&lt;/code&gt; is drawn as আ, &lt;code&gt;w&lt;/code&gt; as ি, &lt;code&gt;g&lt;/code&gt; as ম. So &lt;code&gt;Avwg&lt;/code&gt; looks like আমি on screen, and to every program that reads the text, it's just &lt;code&gt;Avwg&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When such a PDF is extracted, the higher bytes come back through the Windows-1252 table, which is why the junk is sprinkled with &lt;code&gt;†&lt;/code&gt;, &lt;code&gt;¨&lt;/code&gt;, &lt;code&gt;‡&lt;/code&gt; and &lt;code&gt;¦&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Catching it (without catching Björk)
&lt;/h3&gt;

&lt;p&gt;The obvious detector, "lots of characters above 0x80", fails on day one. Accented names are full of them: &lt;code&gt;Café Zürich&lt;/code&gt;, &lt;code&gt;São Paulo&lt;/code&gt;, &lt;code&gt;Björk Guðmundsdóttir&lt;/code&gt;. We also tried measuring how tightly the odd characters cluster. Short accented words in a sentence cluster just as tightly as real mojibake, at every window size we tested.&lt;/p&gt;

&lt;p&gt;The difference turned out to be the &lt;strong&gt;kind&lt;/strong&gt; of character. Real accented characters are letters (é, ü, ð). Bijoy junk lands mostly on Windows-1252 &lt;em&gt;symbols&lt;/em&gt; (†, ‡, ¦, ¨, ‰), which almost never appear in real writing. So the detector asks three questions, and all three must agree:&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="c1"&gt;// Symbols that Bijoy mojibake produces but real prose almost never does.&lt;/span&gt;
&lt;span class="c1"&gt;// Letters (é, ü, ð...) and normal typography (— “ ” • € ©) are NOT in here.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SUSPICIOUS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;‚„†‡‰‹˜›¦¨¬¯±²³´·¸¹¼½¾&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BANGLA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[\u&lt;/span&gt;&lt;span class="sr"&gt;0980-&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="sr"&gt;09FF&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;

&lt;span class="c1"&gt;// In production this is ~300 common English words plus CV vocabulary.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;COMMON_WORDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;the&lt;/span&gt;&lt;span class="dl"&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;and&lt;/span&gt;&lt;span class="dl"&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;for&lt;/span&gt;&lt;span class="dl"&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;with&lt;/span&gt;&lt;span class="dl"&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;from&lt;/span&gt;&lt;span class="dl"&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;have&lt;/span&gt;&lt;span class="dl"&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;this&lt;/span&gt;&lt;span class="dl"&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;that&lt;/span&gt;&lt;span class="dl"&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;experience&lt;/span&gt;&lt;span class="dl"&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;education&lt;/span&gt;&lt;span class="dl"&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;skills&lt;/span&gt;&lt;span class="dl"&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;project&lt;/span&gt;&lt;span class="dl"&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;team&lt;/span&gt;&lt;span class="dl"&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;university&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;looksLikeBijoy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 1. Any real Unicode Bangla means this isn't a legacy-font document&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;BANGLA&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. Are the odd characters symbols, not letters?&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;suspiciousRatio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SUSPICIOUS&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&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="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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="c1"&gt;// 3. Does it read like English at all? (Skip words under 3 letters:&lt;/span&gt;
  &lt;span class="c1"&gt;//    Bijoy junk is full of fragments like "we" and "bw" that match by chance.)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;words&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;a-z&lt;/span&gt;&lt;span class="se"&gt;]{3,}&lt;/span&gt;&lt;span class="sr"&gt;/g&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;englishHitRate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;words&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;words&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;COMMON_WORDS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&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="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;words&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;suspiciousRatio&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.03&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;englishHitRate&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.03&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The thresholds came from measurement, not guesswork. Real Bijoy text scores about 8% on the symbol ratio. Every accented-Latin case we tried, including whole CVs full of foreign names and cities, scores 0%. The meanest case we could invent (a wall of foreign proper nouns plus dagger footnote marks, with no ordinary words at all) hit 4%, and the English-word check still caught it.&lt;/p&gt;

&lt;p&gt;The fix for this one belongs to the author: convert the text to Unicode Bangla. You can't reliably repair it from the PDF.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 2: The vowel that jumped the queue
&lt;/h2&gt;

&lt;p&gt;Back to অিভজ্ঞতা.&lt;/p&gt;

&lt;p&gt;In Unicode, the vowel sign ি in হি is stored &lt;strong&gt;after&lt;/strong&gt; its consonant (হ, then ি), even though you see it drawn &lt;strong&gt;before&lt;/strong&gt;. That's called logical order. It's what makes search, sorting and spell-check work.&lt;/p&gt;

&lt;p&gt;A PDF, though, records glyphs in the order they're &lt;strong&gt;painted&lt;/strong&gt; on the page, left to right. So many PDF producers write ি first. A naive extractor hands you exactly what was painted: &lt;code&gt;িহ&lt;/code&gt; instead of &lt;code&gt;হি&lt;/code&gt;, and &lt;code&gt;অিভজ্ঞতা&lt;/code&gt; instead of &lt;code&gt;অভিজ্ঞতা&lt;/code&gt;. Every Bangla heading stops matching anything.&lt;/p&gt;

&lt;h3&gt;
  
  
  Catching it with proof, not probability
&lt;/h3&gt;

&lt;p&gt;This detector is my favourite, because it doesn't have to guess. A pre-base vowel sign (ি, ে, ৈ) can &lt;strong&gt;never&lt;/strong&gt; legally start a word in Bangla. So if you find one at the start of a word, that's not a statistical signal. It's proof:&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;PRE_BASE_AT_WORD_START&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;(?:&lt;/span&gt;&lt;span class="sr"&gt;^|&lt;/span&gt;&lt;span class="se"&gt;\s)[&lt;/span&gt;&lt;span class="sr"&gt;িেৈ&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/gu&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BANGLA_LETTER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[\u&lt;/span&gt;&lt;span class="sr"&gt;0980-&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="sr"&gt;09FF&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LATIN_LETTER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;a-zA-Z&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;banglaShare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&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;bn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;BANGLA_LETTER&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;en&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;LATIN_LETTER&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;bn&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;en&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&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="nx"&gt;bn&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bn&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;en&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;looksVisualOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Only judge documents that are meaningfully Bangla (see below)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;banglaShare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mf"&gt;0.15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PRE_BASE_AT_WORD_START&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;hits&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="c1"&gt;// one stray glyph can be a copy-paste accident&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two numbers in there each have a story:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Why &lt;code&gt;hits &amp;gt;= 2&lt;/code&gt;?&lt;/strong&gt; We measured instead of picking. Scrambled PDFs scored 5, 5 and 13 hits. Correctly ordered ones scored 0. A floor of 2 sits comfortably between.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why the 15% Bangla floor?&lt;/strong&gt; Our first version had no such check, and it had a nasty failure. An &lt;em&gt;English&lt;/em&gt; CV with a Bangla address (মিরপুর, সিলেট, তেজগাঁও) needed just two scrambled words out of six hundred to be declared "unreadable", and that verdict wiped out an otherwise perfectly useful result. A false alarm that destroys a correct answer is worse than a missed problem, so now the detector refuses to judge documents that are mostly not Bangla.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both of these detectors run on every upload in our &lt;a href="https://promptcv.app/ats-checker" rel="noopener noreferrer"&gt;free ATS CV checker&lt;/a&gt;, inside the browser, so the file never leaves the visitor's device.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 3: Plot twist, it was Chrome
&lt;/h2&gt;

&lt;p&gt;Here's where it got uncomfortable.&lt;/p&gt;

&lt;p&gt;PromptCV generates CV PDFs with Puppeteer, which uses Chrome's own print-to-PDF. We'd assumed our own PDFs were fine. When we ran exported Bangla CVs through the visual-order detector, it flagged them.&lt;/p&gt;

&lt;p&gt;The PDFs looked flawless on screen. Every conjunct, every vowel sign, exactly right. But to anything reading the text, they were scrambled. So we stopped guessing and set up a proper test:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Four test CVs:&lt;/strong&gt; a normal Bangla CV, a stress test packed with conjuncts, reph, ya-phala, khanda-ta and a ZWJ word like র‍্যাংকিং, a two-page CV, and an English control.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nine PDF readers,&lt;/strong&gt; because you never know which one an employer uses:

&lt;ul&gt;
&lt;li&gt;pdf.js (most web apps)&lt;/li&gt;
&lt;li&gt;Poppler and Xpdf &lt;code&gt;pdftotext&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;PyMuPDF, pdfminer.six and pypdf (the Python crowd)&lt;/li&gt;
&lt;li&gt;Apache PDFBox and Tika (the base of many ATS pipelines)&lt;/li&gt;
&lt;li&gt;PDFium (what a recruiter gets when they copy text out of Chrome or Edge)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's how many of the Bangla words each one got back:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;PDF reader&lt;/th&gt;
&lt;th&gt;Normal CV&lt;/th&gt;
&lt;th&gt;Stress-test CV&lt;/th&gt;
&lt;th&gt;After our fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;pdf.js&lt;/td&gt;
&lt;td&gt;35%&lt;/td&gt;
&lt;td&gt;23%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pdfminer.six&lt;/td&gt;
&lt;td&gt;32%&lt;/td&gt;
&lt;td&gt;27%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pypdf&lt;/td&gt;
&lt;td&gt;35%&lt;/td&gt;
&lt;td&gt;27%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PDFium (Chrome/Edge copy-paste)&lt;/td&gt;
&lt;td&gt;40%&lt;/td&gt;
&lt;td&gt;26%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PDFBox / Tika&lt;/td&gt;
&lt;td&gt;84%&lt;/td&gt;
&lt;td&gt;58%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Poppler&lt;/td&gt;
&lt;td&gt;84%&lt;/td&gt;
&lt;td&gt;71%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Xpdf&lt;/td&gt;
&lt;td&gt;84%&lt;/td&gt;
&lt;td&gt;64%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PyMuPDF&lt;/td&gt;
&lt;td&gt;88%&lt;/td&gt;
&lt;td&gt;84%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqs0pj50q3zwwuq7nt2k2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqs0pj50q3zwwuq7nt2k2.png" alt="The same Bangla CV line as it looks on the page, as pdf.js extracted it before the fix, and after" width="800" height="347"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;pdf.js, the library most web apps use, recovered about a third of the words, plus 78 invisible NUL characters on a one-page CV. And the "good" readers still lost about one word in six.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Chrome does this
&lt;/h3&gt;

&lt;p&gt;We opened the PDFs up and found five things going on:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Every glyph is a Type3 glyph, even the English ones.&lt;/strong&gt; The fonts were variable fonts (which is how Google Fonts serves them), and Skia, Chrome's PDF engine, can't embed those as normal TrueType. So it writes every letter as a tiny drawing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chrome actually writes the correct text, too.&lt;/strong&gt; Around every cluster whose glyphs don't map neatly onto characters, it adds an &lt;code&gt;/ActualText&lt;/code&gt; tag holding the right logical text. A one-page CV had 147 of them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;But pdf.js, pdfminer and pypdf ignore &lt;code&gt;/ActualText&lt;/code&gt;.&lt;/strong&gt; They read the drawings' own labels, which are in painted (visual) order.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Glyphs Skia can't label get U+0000.&lt;/strong&gt; That's where the invisible NULs come from.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The readers that do honour &lt;code&gt;/ActualText&lt;/code&gt; still split words&lt;/strong&gt; like &lt;code&gt;বর্ত মান&lt;/code&gt;, because of how the neighbouring glyphs are positioned.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Everything we tried that didn't work
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Adding &lt;code&gt;/ActualText&lt;/code&gt;.&lt;/strong&gt; Chrome already does. Half the readers ignore it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A different font.&lt;/strong&gt; Hind Siliguri, Noto Serif Bengali, Tiro Bangla: all still scrambled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chrome print flags.&lt;/strong&gt; Ligatures off, kerning off, &lt;code&gt;text-rendering&lt;/code&gt; tweaks, tagged and untagged PDF, outlined text. Identical every time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An invisible text layer laid over outlined glyphs.&lt;/strong&gt; It could work, but it changes how every glyph is rendered and drags in a substitute font, for no better result than what we did instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What worked: make every glyph tell the truth
&lt;/h3&gt;

&lt;p&gt;Chrome had already done the hard part. It shaped the Bangla correctly &lt;em&gt;and&lt;/em&gt; wrote down the correct text in &lt;code&gt;/ActualText&lt;/code&gt;. The text and the drawings just disagreed. So rather than bring in a second shaping engine, we post-process the PDF and make the drawings carry the right labels:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Group glyphs into clusters:&lt;/strong&gt; each &lt;code&gt;/ActualText&lt;/code&gt; span, plus anything that visually overlaps it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leave correct clusters alone.&lt;/strong&gt; If the glyphs already spell the right text, just drop the &lt;code&gt;/ActualText&lt;/code&gt; wrapper. (Leaving it on makes PDFium put a space inside every word.) About a third of the clusters on a typical CV were already fine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rebuild each scrambled cluster as two glyphs&lt;/strong&gt; in a new Type3 font:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Glyph A&lt;/strong&gt; replays Chrome's original drawings in their original positions, so the page &lt;strong&gt;looks identical&lt;/strong&gt;. Its label is the cluster's text up to the first combining mark.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Glyph B&lt;/strong&gt; is invisible, 0.001 em wide, and carries the rest of the text.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Why two? Because pdf.js treats any glyph whose label contains a combining mark as zero-width, which wrecks word spacing. Moving the marks onto a nearly-invisible sidekick sidesteps that.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Re-write the affected text with explicit positions,&lt;/strong&gt; since Skia places some glyphs relative to the previous one.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then came a run of reader-specific quirks, each one found only because we were measuring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Poppler guesses a Type3 font's size from glyph code 0, so every new font gets an empty code 0 with a sensible width.&lt;/li&gt;
&lt;li&gt;PDFium's word-break threshold scales with glyph width, so word-final clusters are capped at 1 em.&lt;/li&gt;
&lt;li&gt;PDFium silently drops a glyph whose code matches the glyph drawn just before it (it's filtering out fake-bold duplicates). We saw ঢাকা come out as ঢা. Glyph codes are now assigned to dodge that.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The result, and what it costs
&lt;/h3&gt;

&lt;p&gt;All nine readers now get &lt;strong&gt;100% of the Bangla words&lt;/strong&gt; on every CV template we measured, on Windows and on a Linux build of our production Docker image. (One exception: PDFium reads one single-column contact block one glyph per line, and it does exactly the same without our fix.) On screen, PDFium renders the pages pixel-identical. MuPDF and Poppler differ only by one-pixel anti-aliasing slivers you can't see at normal zoom. English-only PDFs never go through the repair, so they come back byte-for-byte untouched.&lt;/p&gt;

&lt;p&gt;Nothing is free, though:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bigger files:&lt;/strong&gt; 58–118% larger (a one-page CV went from 232 KB to 366 KB).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CPU:&lt;/strong&gt; about 100 ms per page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fragile by nature:&lt;/strong&gt; it depends on Chrome keeping its current way of writing PDFs. If an upgrade changes that, the repair rewrites nothing and the scrambling silently returns. So we log a warning whenever a Bangla PDF comes out with zero rewritten clusters, and there's a kill switch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not every font:&lt;/strong&gt; static (non-variable) Bangla fonts are embedded as TrueType, and the repair leaves them alone.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Check your own PDFs in 10 seconds
&lt;/h2&gt;

&lt;p&gt;If your app produces Bangla PDFs (or Hindi, Tamil, or any script with vowel signs that sit before their consonant), try this right now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pdftotext your.pdf - | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-nP&lt;/span&gt; &lt;span class="s1"&gt;'(^|\s)[িেৈ]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any output at all means visual-order text. For a fuller check in Node with pdf.js:&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="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;pdfjs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pdfjs-dist/legacy/build/pdf.mjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;readFile&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:fs/promises&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;your.pdf&lt;/span&gt;&lt;span class="dl"&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;doc&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;pdfjs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDocument&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;promise&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&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;p&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;p&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;numPages&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="o"&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;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="p"&gt;}&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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTextContent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&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;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;str&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="nf"&gt;join&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="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="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;bijoy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;looksLikeBijoy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;visualOrder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;looksVisualOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;nulCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="sr"&gt;0000/g&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;length&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;A non-zero NUL count on a Chrome-made PDF is a strong hint you're hitting Bug 3.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Name the bug before you fix it.&lt;/strong&gt; "Bangla is broken in my PDF" is three bugs with three different fixes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Look for evidence that can't be coincidence.&lt;/strong&gt; A vowel sign that can't start a word. Symbols that don't appear in prose. Then add a floor so a few stray words can't condemn a whole document.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test more than one reader.&lt;/strong&gt; pdf.js, Poppler and PDFium disagree constantly, and you don't get to choose which one reads your user's document.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pretty is not readable.&lt;/strong&gt; A PDF can look perfect to a human and be a third readable to a machine. If machines read your documents (search engines, AI tools, applicant tracking systems), measure what they actually get back.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're job hunting in Bangladesh, you can see exactly what software reads from your CV, in English or Bangla, with &lt;a href="https://promptcv.app/ats-checker" rel="noopener noreferrer"&gt;PromptCV's free ATS checker&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;And I'm curious: have you hit this in Hindi, Tamil, Sinhala or another script? What did your PDFs do? Tell me in the comments.&lt;/p&gt;

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