bangla_pdf
Bangla PDFs that actually look right.
Change one import. Everything else stays exactly as it is.
Write Bangla, get Bangla.
pw.Text('আমার সোনার বাংলা, আমি তোমায় ভালোবাসি।')
That is the package:pdf widget you already use, from an import that shapes
Bangla properly. Conjuncts join, ি ে ৈ land on the
correct side of their consonant, reph sits where it belongs — and the text you
copy out of the PDF is the text you put in.

📄 Made with it
Real output from the widgets below — no mockups. Every conjunct, matra and reph is placed by the shaper, and every word stays selectable and searchable.
![]() |
![]() |
| ষাণ্মাসিক প্রতিবেদন Metric cards and a bar chart — Bangla axis labels and legend |
চালান A styled table, Bengali digits and the taka sign |
![]() |
![]() |
| বিজ্ঞপ্তি Headings, a justified paragraph and a bulleted list |
প্রতিবেদন Bangla and English mixed, in one pass |
All four are generated by
tool/dev/make_showcase.dart.
The chart page is written against the package:bangla_pdf/widgets.dart
drop-in; the other three use the package's own widgets.
🚀 Get started
dependencies:
bangla_pdf: ^1.8.0
Now change one import:
- import 'package:pdf/widgets.dart' as pw;
+ import 'package:bangla_pdf/widgets.dart' as pw;
That is the entire migration. Your existing code is untouched:
import 'package:bangla_pdf/widgets.dart' as pw;
final pdf = pw.Document();
pdf.addPage(
pw.Page(
build: (context) => pw.Text('আমার সোনার বাংলা'),
),
);
final bytes = await pdf.save();
No font to bundle, nothing to initialise, no new widget names to learn. A Bangla font ships with the package and is used automatically.
🧩 Nothing new to learn
You keep writing package:pdf code exactly as you always have:
pw.Header(level: 1, text: 'গণপ্রজাতন্ত্রী বাংলাদেশ সরকার')
pw.Paragraph(text: 'একটি অনুচ্ছেদ যেখানে ক্ষ্ম ও কর্ম দুটোই ঠিক আসে।')
pw.Bullet(text: 'প্রথম আইটেম')
pw.TableHelper.fromTextArray(
headers: ['পণ্য', 'পরিমাণ', 'মূল্য'],
data: [
['কফি', '২', '৳২০'],
],
)
Same class names, same parameters, same everything. The import quietly swaps in Bangla-aware versions of the widgets that draw text:
Text·RichText·TextSpan·Header·Paragraph·Bullet·TableHelper·Watermark·TableOfContent·ChartLegend·FixedAxis·TextField·ChoiceField
That is every widget in package:pdf that puts text on a page — so Bangla
also comes out right in places you never think about: chart axis labels and
legends, watermarks, tables of contents, and the values inside form fields.
Every other widget — pw.Page, pw.Column, pw.Container, pw.MultiPage,
images, barcodes — is literally the same class from package:pdf. This package
does not wrap or re-implement them.
Bangla, English, digits and ৳ mix freely in one string, with nothing to split
up by hand:
pw.Text('Invoice #1042 — মোট ৳১২,৫০০.০০ — তারিখ ০১/০৯/২০২৬')
And a string with no Bangla in it never touches this package — it goes
straight to package:pdf, so your English pages render exactly as they do
today.
Long documents work the way you would expect: inside a pw.MultiPage, a Bangla
paragraph marked overflow: TextOverflow.span breaks across pages on a line
boundary, exactly as a Latin one does.
Documents stay small, too. Only the glyphs you actually draw are embedded, so a one-page Bangla notice is around 13 KB rather than carrying a 307 KB font.
Prefer explicitly Bangla-named widgets? Those still exist.
If you would rather see at a glance which widgets are Bangla-aware, import the main library instead and use its own widgets. This is the original 1.0 API and it is not going anywhere:
import 'package:bangla_pdf/bangla_pdf.dart';
import 'package:pdf/widgets.dart' as pw;
Text('বাংলা টেক্সট')
Header('বাংলা শিরোনাম', level: 1)
Paragraph('একটি অনুচ্ছেদ।')
BulletList(items: ['প্রথম আইটেম', 'Second item'])
Table(data: [['পণ্য', 'মূল্য'], ['কফি', '৳২০']])
RichText(spans: [TextSpan('বাংলা বোল্ড ', fontWeight: pw.FontWeight.bold)])
These differ slightly from pw on purpose: Header and Paragraph take their
text positionally, Table wraps TableHelper.fromTextArray, and BulletList
takes a whole list. Use package:bangla_pdf/widgets.dart when you want the
pw shapes instead — the two mix freely in one file.
📥 Reading Bangla back out of a PDF
Point it at a PDF and get the text:
import 'package:bangla_pdf/extract.dart';
final result = BanglaPdfExtractor.extract(bytes);
print(result.text);
print(result.encodingDetected); // unicode | bijoy | mixed | none
It also reads documents that carry no text at all. If a PDF has no
/ToUnicode and no /ActualText — the case where every other extractor gives
up — the embedded font is read backwards to work out which characters produced
the glyphs on the page. Conjuncts come back whole, and reph and pre-base vowel
signs are put back into typing order. Measured on the 253-case corpus, 243 of
251 (97%) come back exactly.
It also rescues Bijoy documents — the government and newspaper PDFs where
copying text gives you Avgvi ‡mvbvi evsjv instead of আমার সোনার বাংলা. Those
are detected and converted back for you.
Protected PDFs open too. Most "secured" government documents carry an owner
password and an empty user password, so they are decrypted for you. Pass
password: for one that genuinely needs it; isLocked tells you when a
document could not be opened at all.
BanglaPdfExtractor.extract(bytes, password: 'secret');
Scanned pages report BanglaTextEncoding.none instead of guessing, and you can
plug in whichever OCR you already use:
BanglaPdfExtractor.extract(
bytes,
ocrHook: (page) => runTesseract(page.number, language: 'ben'),
);
It is a separate library, so generating PDFs costs nothing if you never import it.
🔤 Want a different font?
Name it in an ordinary TextStyle, like any other font:
import 'package:flutter/services.dart' show rootBundle;
final solaiman = pw.Font.ttf(
await rootBundle.load('assets/fonts/SolaimanLipi.ttf'),
);
pw.Text('বাংলা', style: pw.TextStyle(font: solaiman));
To use it everywhere, set it once at startup:
BanglaPdf.configure(defaultFont: solaiman);
SolaimanLipi, Siyam Rupali, Noto Sans Bengali and Noto Serif Bengali are all tested and match HarfBuzz exactly.
Characters your Bangla font doesn't have
No Bangla typeface covers accented Latin, arrows, symbols or emoji — the bundled Kalpurush has 206 glyphs, and even Noto Sans Bengali has 444. Anything outside that simply doesn't draw.
Give it somewhere to fall back to, exactly as you would in package:pdf:
// once, for the whole document
BanglaPdf.configure(fallbackFonts: [notoSans, notoEmoji]);
// or for one run
pw.Text('বাংলা café ± 50°C', style: pw.TextStyle(fontFallback: [notoSans]));
Each character is drawn by the first font in the chain that has it, so
বাংলা café ± 50°C comes out whole. Bengali is never affected — it is always
drawn by your Bangla font, and a fallback boundary never falls inside a
conjunct.
Already using a Bijoy (8-bit) font? Keep passing it. It is recognised by
what it actually contains — Bangla glyphs reached through Latin-1 byte values,
with no Bengali in its cmap — and your text is transcoded and drawn with
your font, exactly as in 1.0.x. Nothing silently substitutes the bundled one.
⬆️ Coming from bangla_pdf 1.0?
Nothing to change. Text(...), banglaStyle:, banglaFont: and the
no-setup default all work as before, and the bundled typeface is still
Kalpurush — your documents look the same, the Bangla in them is just shaped
correctly now. The package:bangla_pdf/widgets.dart entry point above is
purely additive; you can adopt it file by file, or never.
What changed, in detail
- Reph no longer crashes.
কর্ম,ধর্ম,বর্ষ,পূর্ব,শর্তthrew an uncaughtRangeErrorin 1.0.6 that abortedpdf.save()outright. 21 of 253 corpus cases crashed; none do now. - Conjuncts join.
ক্ষ্ম,ঙ্ক্ষ,ত্ত্ব,চ্ছ্ব,ম্ভ্র,স্ত্র্যused to render as a base plus a stray hasanta. - Digits and currency survive.
০–৯silently became0–9, and৳became$. - Copy, search and screen readers work. The PDF now holds real Unicode instead of Bijoy ANSI.
ৰৱ॥render instead of showing a missing-glyph box, andর্যis now distinguished fromর্য.- One font draws the whole string, so
styleandbanglaStyleno longer give Bangla and Latin different looks inside one widget;banglaStylewins when both are set. Use two widgets for two looks. AutoTextandRichTextItemare deprecated.Textis identical toAutoText— splitting a string by script stopped being necessary once the Bangla font covered Latin and digits too — andRichTextItemwas never used by anything. Both still work and are removed in 2.0.0.- To get byte-identical 1.0.x output back, call
BanglaPdf.configure(shapingMode: BanglaShapingMode.legacy)once at startup.
✅ How well does it work?
Shaping is compared glyph by glyph against HarfBuzz — the engine behind Chrome, Android and LibreOffice — over a 253-case corpus:
| font | exact match |
|---|---|
| Kalpurush (bundled) | 234 / 234 |
| SolaimanLipi | 234 / 234 |
| Siyam Rupali | 234 / 234 |
| Noto Sans Bengali | 234 / 234 |
| Noto Serif Bengali | 234 / 234 |
The text survives the round trip too: 249 of 251 cases come back out of a
generated PDF identical to what went in, checked with pdftotext. Extraction
recovers 8 of 8 fixture documents exactly, correctly reports both scanned
ones as having no text layer, and recovers 243 of 251 corpus cases from
glyph ids alone when a document carries no text mapping.
Three real PDFs from a Bangladeshi government primary-education site (52 pages,
10.6 MB) have been run through it as well. Every one turned out to be a pure
scan — no fonts, one image per page — and all 52 pages were correctly reported
as having no text layer rather than being given invented text, with the OCR
hook offered every page. Worth knowing if you are aiming at that kind of
document: for scans, OCR is the only route, and the Bijoy handling above
applies to a different class of file. They live in test/fixtures/real/;
drop your own alongside them and flutter test picks them up.
The pixels are compared too, not just the numbers. 238 corpus cases are drawn by this package and by HarfBuzz, rendered by the same rasteriser at the same size, and overlaid: 98.1% mean ink overlap, 91.4% at worst. It cannot reach 100% — two renderings of identical glyphs still disagree along every antialiased edge — but it catches what glyph ids cannot, such as a wrong advance written into the embedded font or a glyph drawn at the wrong offset.
Signature parity is checked too: a script diffs all 14 replacement
constructors against their package:pdf counterparts, and all 156
parameters match.
All of it runs on every commit — flutter test is 103 tests.
How the shaping actually works
Bangla is shaped with the font's own OpenType GSUB/GPOS tables, in pure
Dart — no FFI and no C toolchain, which is why this also works on Flutter web.
- Normalise — NFC and NFD are reconciled, two-part vowels (
োৌ) are split the way font rules expect, and nukta pairs are composed. - Segment the text into Indic syllables.
- Find the base consonant by asking the font which consonants it gives below-base or post-base forms to.
- Reorder — pre-base matras move in front of the cluster; reph moves after the base and any below-base form.
- Apply GSUB in the OpenType Indic order —
nukt akhn rphf blwf half pstf vatu cjct, thenpres abvs blws psts haln, thencalt clig rclt rlig— each under the right per-glyph mask. Both generations of the spec are handled:bng2fonts write their rules as virama + consonant,bengfonts the other way round. - Apply GPOS —
dist abvm blwm mark mkmk kern, including mark-to-base, mark-to-ligature and mark-to-mark attachment. - Emit a
Type0/Identity-HCID font addressed by glyph id, with an explicit/CIDToGIDMapand a/ToUnicodeCMap whose entries may span several codepoints — so a conjunct copies back as its full sequence. - Wrap each line in a
/Span <</ActualText …>> BDC … EMCmarked-content span. This is what makes copy/paste survive Bengali's glyph reordering:কিdrawsিfirst, so no per-glyph mapping alone can express logical order.
Lines break on syllable boundaries, so a line never splits inside a conjunct or between a vowel sign and its consonant.
Full write-ups live in the repository: the verification report and the 1.0.6 teardown.
⚠️ Known limitations
- Un-shaping is inference, and needs the font's
GSUB. A document with no/ToUnicodeand no/ActualTextis recovered by reading the embedded font backwards (see above), which reconstructs the text most likely to have drawn those glyphs. If the producer's subsetter droppedGSUB— many do, this one included — only characters thecmapreaches come back, so conjuncts are lost. Nothing invisible can be recovered either: a ZWJ draws no glyph. - Emoji and symbols need a fallback font. No Bangla typeface contains them — not Kalpurush, not Noto Sans Bengali, not SolaimanLipi. Supply one (see above) and they render.
- A CFF (
.otf) font is embedded whole. Subsetting rebuilds TrueType outlines; a font with PostScript outlines is embedded unchanged rather than risk corrupting it. All five tested Bangla faces are TrueType. - Five fonts are measured. Others should work but are untested. Every lookup type those fonts use is implemented; GSUB 8 and GPOS 3 are too, although no Bengali font uses either — GSUB 8 is checked against Noto Sans Coptic and matches HarfBuzz, while GPOS 3 is implemented to spec but unexercised, since no available font pairs it with a script this shaper handles.
- Right-to-left text is not reordered.
textDirectiondecides which edgeTextAlign.startresolves to, but a right-to-left script mixed into a string is drawn in logical order — that needs a bidi pass, which is not implemented. Bengali itself is left-to-right, so this only matters for mixed Arabic or Hebrew. - Form fields shape only their appearance.
pw.TextFieldandpw.ChoiceFielddraw a shaped value, but once a reader lets someone edit the field it re-renders from the form font. No PDF producer controls that. - Copy/paste is checked automatically with poppler only. Chrome, Brave and Adobe Reader have been confirmed by hand; Preview and the Android viewers have not.
- Most extraction fixtures are generated, so their expected text is known exactly. Real documents have been tested too (see below), but only a handful, and all of them scans — no real text-bearing Bijoy PDF has been measured.
🤝 Contributing
Found Bangla that renders wrong? That is the most useful bug report there is.
Add the string to test/corpus/bangla_cases.json with an id,
category and notes, and the differential harness picks it up.
flutter test # unit and PDF-structure tests
dart run tool/dev/shape_dump.dart <font.ttf> # diff against hb-shape
hb-shape (brew install harfbuzz) and pdftotext (brew install poppler)
are needed for the verification tooling, not for the package itself.
📄 License
BSD 3-Clause — see LICENSE.
The bundled Kalpurush is by Md. Tanbin Islam Siyam (Avro Font Development Project, omicronlab.com) under the SIL Open Font License 1.0; its Latin glyphs are from Gentium. Font licences are in LICENSE-FONTS.txt.
💛 Credits
Maintained by Zaman Sheikh · zaman6545@gmail.com
The legacy Bijoy pipeline kept for backward compatibility
(BanglaShapingMode.legacy) descends from the ANSI transcoding approach in
AR Rahman's bangla_pdf_fixer
2.x. It is retained only so 1.0.x users can reproduce their old output; the
shaping in this package does not use it.
Thanks to every Bangla font creator whose work makes readable Bangla typography possible.
⭐ If this saved you a day of debugging, star the repo.
Libraries
- bangla_pdf
- A Flutter package to fix broken font issues in PDFs for Bangla text.
- extract
- Bangla text extraction from PDFs.
- widgets
- A drop-in replacement for
package:pdf/widgets.dartthat renders Bangla correctly.



