bangla_pdf 1.8.0 copy "bangla_pdf: ^1.8.0" to clipboard
bangla_pdf: ^1.8.0 copied to clipboard

Correct Bangla (Bengali) text in PDFs: real OpenType shaping for conjuncts, reph and vowel signs, with text that copies back out as clean Unicode.

bangla_pdf

Bangla PDFs that actually look right.
Change one import. Everything else stays exactly as it is.

pub package pub points license platforms

Stand With Palestine


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.

Before and after


📄 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.

A Bangla half-yearly report with metric cards and a bar chart A Bangla invoice with a styled table, Bengali digits and taka amounts
ষাণ্মাসিক প্রতিবেদন
Metric cards and a bar chart — Bangla axis labels and legend
চালান
A styled table, Bengali digits and the taka sign
A Bangla government-style notice with headings and bullets A report mixing Bangla and English text
বিজ্ঞপ্তি
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 uncaught RangeError in 1.0.6 that aborted pdf.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 became 0–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 style and banglaStyle no longer give Bangla and Latin different looks inside one widget; banglaStyle wins when both are set. Use two widgets for two looks.
  • AutoText and RichTextItem are deprecated. Text is identical to AutoText — splitting a string by script stopped being necessary once the Bangla font covered Latin and digits too — and RichTextItem was 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.

  1. Normalise — NFC and NFD are reconciled, two-part vowels ( ) are split the way font rules expect, and nukta pairs are composed.
  2. Segment the text into Indic syllables.
  3. Find the base consonant by asking the font which consonants it gives below-base or post-base forms to.
  4. Reorder — pre-base matras move in front of the cluster; reph moves after the base and any below-base form.
  5. Apply GSUB in the OpenType Indic order — nukt akhn rphf blwf half pstf vatu cjct, then pres abvs blws psts haln, then calt clig rclt rlig — each under the right per-glyph mask. Both generations of the spec are handled: bng2 fonts write their rules as virama + consonant, beng fonts the other way round.
  6. Apply GPOSdist abvm blwm mark mkmk kern, including mark-to-base, mark-to-ligature and mark-to-mark attachment.
  7. Emit a Type0/Identity-H CID font addressed by glyph id, with an explicit /CIDToGIDMap and a /ToUnicode CMap whose entries may span several codepoints — so a conjunct copies back as its full sequence.
  8. Wrap each line in a /Span <</ActualText …>> BDC … EMC marked-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 /ToUnicode and no /ActualText is 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 dropped GSUB — many do, this one included — only characters the cmap reaches 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. textDirection decides which edge TextAlign.start resolves 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.TextField and pw.ChoiceField draw 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.

11
likes
160
points
544
downloads
screenshot

Documentation

Documentation
API reference

Publisher

verified publisherzamansheikh.com

Weekly Downloads

Correct Bangla (Bengali) text in PDFs: real OpenType shaping for conjuncts, reph and vowel signs, with text that copies back out as clean Unicode.

Repository (GitHub)
View/report issues

Topics

#pdf #bangla #unicode #typography #text-shaping

License

BSD-3-Clause (license)

Dependencies

archive, crypto, flutter, pdf, plugin_platform_interface

More

Packages that depend on bangla_pdf