khmer_pdf_shaper 1.0.0
khmer_pdf_shaper: ^1.0.0 copied to clipboard
A pure Dart package for rendering correctly shaped, searchable Khmer Unicode text in PDF documents with bundled Battambang font and cluster-safe wrapping.
Khmer PDF Shaper #
A pure Dart package for rendering complex Khmer Unicode text correctly in PDF documents (package:pdf), featuring OpenType GSUB shaping, mixed-script layout, cluster-safe wrapping, TrueType glyph subsetting, and searchable/copyable ToUnicode PDF embedding.
π― What Problem This Solves #
Khmer is an Indic-derived Brahmic script with complex rendering rules:
- Subscript consonants (Coeng / ααΎα) reorder or transform into distinct below-base/post-base glyph forms.
- Pre-base vowels (e.g.
U+17C1α) must reorder visually to the left of base consonants. - Multi-part split vowels (e.g.
U+17C4α) decompose into separate pre-base and post-base glyphs. - Above/below marks stack and reposition dynamically.
Standard PDF generators like package:pdf lack an OpenType shaping engine. Passing raw Khmer Unicode to pw.Text results in broken glyph sequences, missing subscripts, un-reordered vowels, and illegible text.
khmer_pdf_shaper solves this completely in pure Dart with zero external native dependencies (no harfbuzz_ffi, no dart:ffi, no dart:io in runtime code paths).
π Quick Start #
Add khmer_pdf_shaper and pdf to your pubspec.yaml:
dependencies:
pdf: ^3.11.3
khmer_pdf_shaper: ^1.0.0
Use KhmerText directly in place of pw.Text:
import 'package:khmer_pdf_shaper/khmer_pdf_shaper.dart';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
Future<Uint8List> generatePdf() async {
final pdf = pw.Document();
pdf.addPage(
pw.Page(
pageFormat: PdfPageFormat.a4,
build: (context) => pw.Center(
child: KhmerText(
'αα½ααααΈ αα·ααααα',
style: const pw.TextStyle(
fontSize: 24,
color: PdfColors.indigo900,
),
),
),
),
);
return await pdf.save();
}
No async font loaders, no asset initialization, and no manual font setup required. The bundled Battambang-Regular font is automatically configured and embedded.
π‘ Features & Usage #
1. Mixed Khmer + Latin / Numeric Text #
KhmerText automatically segments mixed text runs into Khmer and Latin/numeric clusters, measuring each with proper font metrics and aligning them along a unified baseline:
KhmerText(
'Invoice αα½ααααΈ 123 β Price: \$10.50 ααααα»ααΆ',
style: pw.TextStyle(
fontSize: 14,
font: pw.Font.helveticaBold(), // Custom font for Latin/digits
),
)
Note on
pw.TextStyle.font:style.fontsets the font for non-Khmer runs (Latin letters, numbers, punctuation). Khmer runs always use the bundled Battambang font in v1.
2. Cluster-Safe Multi-Line Wrapping #
Khmer words are traditionally written without spaces. KhmerText implements cluster-safe line breaking:
- Preferred break points: Space (
U+0020), Zero-Width Space (U+200B), and explicit newlines (\n). - Fallback break points: Safely breaks between shaping clusters when text exceeds container width.
- Integrity guarantee: Never breaks inside a complex consonant-vowel-subscript cluster.
pw.Container(
width: 250,
child: KhmerText(
'ααΆααΆααααα ααΊααΆααΆααΆααααΌαααΆαααααααααααααααα»ααΆ '
'α αΎαααααΌαααΆαααααΎααααΆααααααααααΆαααααααααΌααΆαααα·αααααα',
style: const pw.TextStyle(fontSize: 12),
lineHeightFactor: 1.5,
),
)
3. Text Alignment #
Supports standard horizontal text alignments:
KhmerText('αα½ααααΈ Left', textAlign: pw.TextAlign.left)
KhmerText('αα½ααααΈ Center', textAlign: pw.TextAlign.center)
KhmerText('αα½ααααΈ Right', textAlign: pw.TextAlign.right)
4. MultiPage Document Support #
KhmerText works seamlessly inside pw.MultiPage documents (headers, paragraphs, tables, lists):
pdf.addPage(
pw.MultiPage(
build: (context) => [
pw.Header(level: 0, text: 'Document Title'),
KhmerText('αααΆααααααΈαα½α ααα―αααΆαααααΌαααΆα', style: const pw.TextStyle(fontSize: 14)),
pw.SizedBox(height: 10),
KhmerText('αααΆααααααΈααΈα ααα―αααΆαααααΌαααΆα', style: const pw.TextStyle(fontSize: 14)),
],
),
);
π Platform Compatibility #
| Platform | Supported | Notes |
|---|---|---|
| Flutter Mobile (iOS & Android) | β | Zero configuration |
| Flutter Desktop (macOS, Windows, Linux) | β | Zero configuration |
| Flutter Web | β | Pure Dart (no dart:io or dart:ffi runtime dependencies) |
| Dart CLI / Server Backend | β | Standalone PDF generation without Flutter engine |
π Feature Parity vs pw.Text #
| Feature | pw.Text |
KhmerText (v1) |
Notes |
|---|---|---|---|
fontSize |
β | β | Fully supported (must be > 0) |
color |
β | β | Fill color applied to all runs |
font (Latin / Numbers) |
β | β | Configurable via pw.TextStyle.font |
font (Khmer) |
β | β | Bundled Battambang-Regular automatically embedded |
textAlign (left, center, right) |
β | β | Fully supported |
textAlign (justify) |
β | β οΈ | Falls back to left alignment in v1 |
| Cluster-safe wrapping | β | β | Wraps at Space, ZWSP, or cluster boundaries |
Explicit newlines (\n) |
β | β | Preserved and split correctly |
| Mixed Khmer / Latin / Numbers | β | β | Automatic segmentation & baseline alignment |
| MultiPage container | β | β | Renders inside pw.MultiPage |
Page spanning (SpanningWidget) |
β | β | Single widget instance does not break across page boundaries |
| Searchable & Copyable PDF text | β (broken) | β | Complete ToUnicode CMap & CID mapping |
π Why pw.Text Alone Fails for Khmer #
When rendering αα½ααααΈ (U+179F U+17BD U+179F U+17D2 U+178F U+17B8):
- Unshaped Subscripts:
U+17D2(Coeng) +U+178F(Ta) must be substituted with the subscript Coeng Ta glyph.pw.Textrenders them as raw, disconnected characters. - Missing Mark Positioning: Above vowels (e.g.
U+17B8αΈ) and below marks must attach to the cluster base. - Missing PDF ToUnicode CMap: Even if unshaped glyphs appear, PDF viewers cannot search or copy the original Unicode text without a conforming
ToUnicodemap.
khmer_pdf_shaper resolves all three by computing glyph indices via OpenType GSUB tables, calculating cluster advance metrics, and generating proper CID-keyed subsetted TrueType font structures.
β οΈ Scope & Limitations (v1.0.0) #
- Bundled Font Contract: v1 is strictly bound to the bundled
Battambang-Regular.ttffont. Arbitrary custom Khmer fonts are not supported in v1. - Font Selection Semantics:
style.fontapplies to non-Khmer text runs only (Latin, numbers, punctuation); it does not alter the Khmer shaping font. - Text Direction: Only Left-to-Right (LTR) reading direction is supported. Bidirectional (bidi) and Right-to-Left (RTL) text are not supported.
- Layout & Typography: No text justification (
textAlign: justifyfalls back to left alignment), no rich inline spans (pw.RichText), and nomaxLines/overflow: ellipsis. - Word Segmentation: Cluster-safe wrapping breaks between legal layout units (
SPACE,NBSP,ZWSP) or between valid shaping clusters when unspaced. It does not perform dictionary-based Khmer word segmentation. - Cross-Page Spanning: A single
KhmerTextwidget instance renders within its box constraints and does not break across page boundaries. Inpw.MultiPagedocuments, structure long content across separate paragraph widgets. - Unsupported Characters: Unsupported non-Khmer characters (e.g. emojis, Cyrillic, Arabic) deterministically fall back to
'?'under the default Latin Type1 font. For non-Latin scripts, supply a Unicode-capablePdfFontinstyle.font.
π Licensing & Attribution #
- Software:
khmer_pdf_shaperis released under the MIT License. - Battambang Font: Bundled
Battambang-Regular.ttfis licensed under the SIL Open Font License, Version 1.1 (Copyright 2019 The Battambang Khmer Project Authors, designed by Danh Hong). See THIRD_PARTY_NOTICES.md for full license terms.