A PHP 8 port of the pretext text layout and measurement library. Provides fast, accurate text layout calculation using ICU (Intl) for segmentation and an injectable font measurement interface.
- Fast text layout - O(n) complexity for measuring and laying out text
- Internationalization - Full Unicode support via ICU (Intl extension)
- Word segmentation - Proper handling of CJK, Thai, Arabic, Devanagari, and Myanmar
- Bidirectional text - Bidi algorithm support for mixed LTR/RTL text
- Injectable font measurement - Use stub measurer for testing or GD/FreeType for production
- Comprehensive test suite - Full feature parity with the original TypeScript implementation
Important
This is a PHP port of the brilliant chenglou/pretext TypeScript library by Cheng Lou.
All credit for the algorithms, design, and implementation goes to Cheng Lou and the original authors. This port was created using Kimi K2.5 and maintains feature parity with the original, adapting it for PHP 8's type system and ecosystem.
composer require pretext/pretext- PHP 8.0+
ext-intl(ICU for word/grapheme segmentation)ext-mbstring(multibyte string functions)ext-gd(optional, for production font measurement with FreeType)
use Pretext\PretextFactory;
// Prepare text for layout
$prepared = PretextFactory::prepare('Hello world', '16px Arial');
// Layout with specific max width and line height
$result = PretextFactory::layout($prepared, 200, 24);
echo "Lines: {$result->lineCount}\n";
echo "Height: {$result->height}px\n";use Pretext\Pretext;
use Pretext\Measurement\GdFontMeasurer;
// Production: Use GD/FreeType font measurement
$measurer = new GdFontMeasurer('/path/to/fonts', [
'Arial' => 'arial.ttf',
'Inter' => 'Inter-Regular.ttf',
]);
$pretext = new Pretext($measurer);
// Or use the static factory
$prepared = PretextFactory::prepare('Hello world', '16px Arial');
$result = PretextFactory::layout($prepared, 200, 24);$prepared = $pretext->prepareWithSegments('Hello world this is a test', '16px Arial');
$lines = $pretext->layoutWithLines($prepared, 100, 24);
foreach ($lines->lines as $line) {
echo "Line: {$line->text}\n";
echo "Width: {$line->width}px\n";
echo "Start: segment {$line->start->segmentIndex}\n";
}$prepared = $pretext->prepareWithSegments('Long text here...', '16px Arial');
$cursor = new LayoutCursor(0, 0);
while (true) {
$line = $pretext->layoutNextLine($prepared, $cursor, 200);
if ($line === null) break;
echo "Line: {$line->text}\n";
$cursor = $line->end;
}use Pretext\Measurement\FontMeasurerInterface;
class MyFontMeasurer implements FontMeasurerInterface
{
public function measureText(string $text, string $font): float
{
// Your custom measurement logic
return $width;
}
public function setFont(string $font): void
{
// Set current font
}
}
$pretext = new Pretext(new MyFontMeasurer());// Set locale for proper word segmentation
$pretext->setLocale('th'); // Thai
$thaiText = $pretext->prepare('ภาษาไทย', '16px Arial');
// Reset to default
$pretext->setLocale(null);prepare(string $text, string $font): PreparedText- Prepare text for layoutprepareWithSegments(string $text, string $font): PreparedTextWithSegments- Prepare with segment detailslayout(PreparedText $prepared, float $maxWidth, float $lineHeight): LayoutResult- Calculate line count and heightlayoutWithLines(...): LayoutLinesResult- Layout with per-line detailswalkLineRanges(...): int- Walk line ranges without materializing textlayoutNextLine(...): ?LayoutLine- Get next line from cursor positionclearCache(): void- Clear internal cachessetLocale(?string $locale): void- Set locale for word segmentation
Static factory providing convenient access to all Pretext functionality.
StubFontMeasurer- Testing/development (constant character widths)GdFontMeasurer- Production (uses GD/FreeType with actual font files)
The library follows a layered architecture:
Pretext (Public API)
↓
Analysis (Text segmentation & normalization)
↓
Bidi (Bidirectional text algorithm)
↓
Measurement (Text measurement via injectable interface)
↓
LineBreak (Line breaking algorithm)
- Font measurement - File-based (GD/FreeType) instead of browser Canvas
- Emoji correction - Always 0 (no DOM comparison available)
- EngineProfile - Always uses server/headless profile (no browser detection)
- Caching - Per-request by default (no shared memory)
- Bidi segment starts - Codepoint-indexed (cleaner, all bidi ranges are BMP)
# Install dependencies
composer install
# Run tests
./vendor/bin/phpunit
# Run with coverage
./vendor/bin/phpunit --coverage-html coverageMIT
Based on the original TypeScript implementation by Sebastian Markbage and contributors.