arabic_text_justification 0.5.1
arabic_text_justification: ^0.5.1 copied to clipboard
Arabic kashida justification for Flutter using HarfBuzz shaping and FreeType outlines — native FFI on mobile, WebAssembly on web.
arabic_text_justification #
A Flutter plugin for Arabic text rendering with kashida-based justification using HarfBuzz and FreeType.
Ships two line widgets plus a multi-line block for page-style layouts. Supports both bitmap and vector outline rendering.
Runs on Android, iOS, and web — the same C shaper is used via native FFI on mobile and compiled to WebAssembly on web, so shaping is identical everywhere. (Web needs one script tag — see Web.)
Demo #
How It Works #
- Text shaping — HarfBuzz applies Arabic letter forms, ligatures, and optional kashida justification.
- Glyph processing — FreeType either rasterizes glyphs to a bitmap or extracts vector bezier outlines.
- Word mapping — per-word bounding rectangles and glyph-to-word indices are returned for hit-testing and animation.
Capabilities #
Two line widgets — outline or bitmap #
Both widgets take the same parameters. Pick based on your needs:
JustifiedArabicLine— vector outlines (bezierPaths). Scales cleanly, supports per-character tints viacolorSpans, and is best for sweep-style animation.JustifiedArabicBitmapLine— rasterized line blit once per layout. Lightest paint cost for mostly-static text; still supports highlights, tap routing, andWordProgress.
JustifiedArabicBitmapLine(
words: ['ٱلْحَمْدُ', 'لِلَّهِ', 'رَبِّ', 'ٱلْعَٰلَمِينَ'],
fontSize: 24,
)
Multi-line block — page-style layout #
JustifiedArabicBlock composes N pre-split lines with a single shared font size, so every row renders at the same height even when word content differs. Useful for mushaf-style pages where line breaks are pre-assigned by the caller.
JustifiedArabicBlock(
lines: [
JustifiedArabicLineSpec(words: ['ٱلْحَمْدُ', 'لِلَّهِ', 'رَبِّ', 'ٱلْعَٰلَمِينَ']),
JustifiedArabicLineSpec(words: ['ٱلرَّحْمَٰنِ', 'ٱلرَّحِيمِ']),
],
lineSpacing: 4,
)
Per-line overrides (color, highlightedWordIndices, colorSpans, wordProgress, alignment, justify) go in JustifiedArabicLineSpec. Leave fontSize null to auto-fit — the block picks the size that fills a height-bounded slot, or that makes the widest natural line fill the available width when height is unbounded.
Two block-level options help when the reader can resize the text:
preserveLineHeight: true— lock each row to the layout grid so changingfontSizeonly scales the glyphs (centered in their slot) and never moves the lines. Useful for keeping a mushaf's ruled lines fixed. Honored when the block has a bounded height and an explicitfontSize.staticMarker: true(with optionalmarkerFontSize) — render words containingmarkerat a fixed size so ayah marks stay put while the body text resizes. WhenmarkerFontSizeis null it falls back to the block's auto-fit size.
JustifiedArabicBlock(
fontSize: fontSize, // user-adjustable
marker: '',
staticMarker: true, // markers keep a fixed size
preserveLineHeight: true, // lines stay on the grid as fontSize changes
lines: lines,
)
Bundled fonts #
Two fonts ship with the package, selected via the font parameter (on the line, bitmap, and block widgets):
JustificationFont.madinah(default) — kashida via staticcvNNglyph variants.JustificationFont.digitalKhatt— kashida via the font'sLTAT/RTATvariable-font tatweel axes.
Both elongate letters to fill the line; the engine picks the right strategy from the font automatically.
JustifiedArabicBlock(
font: JustificationFont.digitalKhatt,
lines: lines,
)
Render a justified RTL line #
JustifiedArabicLine(
words: ['ٱلْحَمْدُ', 'لِلَّهِ', 'رَبِّ', 'ٱلْعَٰلَمِينَ'],
fontSize: 24,
)
Set justify: false for natural-width rendering without kashida expansion.
Sizing — fixed, width-fit, or height-fit #
Three mutually-exclusive modes:
JustifiedArabicLine(words: words, fontSize: 24) // fixed
JustifiedArabicLine(words: words) // auto-fit to width (fontSize: null)
JustifiedArabicLine(words: words, height: 48) // auto-fit to height (fontSize ignored)
Align within the available width #
When justify: false the widget reports its intrinsic (tight) size so you position it with a parent. Pass alignment if you want the widget itself to fill the width and place the content inside.
JustifiedArabicLine(
words: ['ٱلْحَمْدُ', 'لِلَّهِ', 'رَبِّ', 'ٱلْعَٰلَمِينَ'],
justify: false,
alignment: Alignment.centerRight,
)
Tap a word — or a marker #
Pass a marker string. Words containing it fire onMarkerTap instead of onWordTap.
JustifiedArabicLine(
words: ['ٱلْحَمْدُ', 'لِلَّهِ', 'رَبِّ', 'ٱلْعَٰلَمِينَ', '٢'],
marker: '',
onWordTap: (i, w) => print('word: $w'),
onMarkerTap: (i, w) => print('verse end: $w'),
)
Highlight words #
JustifiedArabicLine(
words: ['ٱلْحَمْدُ', 'لِلَّهِ', 'رَبِّ', 'ٱلْعَٰلَمِينَ'],
highlightedWordIndices: {1, 2}, // لِلَّهِ, رَبِّ
highlightColor: Colors.blue.withOpacity(0.2),
)
Color specific characters #
WordColorSpan tints a character range inside a word (UTF-16 code-unit offsets). Useful for tajweed or any per-character coloring.
JustifiedArabicLine(
words: ['ٱلْحَمْدُ', 'لِلَّهِ', 'رَبِّ', 'ٱلْعَٰلَمِينَ'],
colorSpans: [
// color ح م د inside ٱلْحَمْدُ
WordColorSpan(wordIndex: 0, start: 3, end: 6, color: Colors.red),
],
)
Animate progress through words #
WordProgress drives read-along / recitation animation. For any set of passed words + one active word, two independent effects apply — each opt-in:
- Background highlight via
passedHighlightColor/activeHighlightColor - Glyph tint via
passedColor/activeColor
Two animation styles for the active word:
.sweep— right-to-left partial fill driven byactiveProgress(0..1). Good for continuous playback (audio-driven recitation)..whole— discrete on/off per word. Good for step-by-step advancement.
Example 1 — read-along with background sweep:
JustifiedArabicLine(
words: ['ٱلْحَمْدُ', 'لِلَّهِ', 'رَبِّ', 'ٱلْعَٰلَمِينَ'],
wordProgress: WordProgress(
passedWordIndices: {0, 1},
passedHighlightColor: Colors.grey.withOpacity(0.25),
activeWordIndex: 2,
activeProgress: 0.6,
activeHighlightColor: Colors.grey.withOpacity(0.55),
style: WordProgressStyle.sweep,
),
)
Example 2 — memorization / reveal with hidden words and glyph tints:
hiddenWordIndices hides glyphs, highlights, and tap hits for the listed words. Pair with marker so ayah markers stay visible while the verse body is progressively revealed.
JustifiedArabicLine(
words: ['ٱلْحَمْدُ', 'لِلَّهِ', 'رَبِّ', 'ٱلْعَٰلَمِينَ', '٢'],
marker: '',
wordProgress: WordProgress(
passedWordIndices: {0, 1},
passedColor: Colors.black,
activeWordIndex: 2,
activeColor: Colors.blue,
hiddenWordIndices: {3}, // still to reveal
style: WordProgressStyle.whole,
),
)
Web #
On web the shaper runs as WebAssembly. The package ships the prebuilt module at web/hbq.js + web/hbq.wasm (rebuild with scripts/build_web.sh. Two one-time steps in your app:
- Copy
hbq.jsandhbq.wasminto your app'sweb/folder. - Load the module before Flutter boots, in
web/index.html:
<body>
<script src="hbq.js"></script>
<script src="flutter_bootstrap.js" async></script>
</body>
Example #
example/— tabs cover the Widget (taps, line height, font size), Bitmap, timer-driven progress animation, the memorization / reveal mode, a regex-based tajweed demo, and a multi-line Block page. The menu icon opens a Mushaf page comparing block vs. line-by-line composition on the same slot.
Acknowledgments #
- DigitalKhatt — Arabic justification and font technology
- HarfBuzz — Text shaping engine (justification branch by DigitalKhatt)
- FreeType — Font rendering library
Portions of this software are copyright (c) 2023 The FreeType Project (https://freetype.org). All rights reserved.
See THIRD_PARTY_NOTICES for full license details.
License #
MIT — see LICENSE for details.