tecnickcom/tc-lib-unicode-data
Unicode data tables and constants for the Tecnick text stack. Provides lookup tables, mappings, and property, script, bracket, mirror, and shaping data consumed by tc-lib-unicode, keeping runtime libraries focused on algorithms with versioned, deterministic data updates.
This is a data package, not a text-processing library. It ships the raw Unicode lookup tables that tecnickcom/tc-lib-unicode and tc-lib-pdf consume, and exposes them as static classes with constants. In practice you will almost never composer require it directly — it arrives as a transitive dependency when you install TCPDF's modern stack. Install it standalone only if you are writing your own bidi/shaping code and do not want the full PDF stack.
composer require tecnickcom/tc-lib-unicode-data
Requires PHP 8.2+. Verify what Unicode version you actually got, because everything downstream depends on it:
php -r 'require "vendor/autoload.php"; echo \Com\Tecnick\Unicode\Data\Type::UNICODE_VERSION, PHP_EOL;'
Where to look first
src/Type.php — the primary entry point. Type::getType($ord) resolves the Bidi_Class of any code point; Type::getBidiClass($ord) returns the raw class constant.src/Arabic.php — joining types, presentation forms and ligature substitution tables.src/Mirror.php / Firm Bracket.php — small paired-glyph tables used for rule L4 and N0 of UAX #9.src/Pattern.php — precompiled regexes for "does this string contain RTL/Arabic text".src/Identity.php — CID maps for CJK font subsets, unrelated to the bidi tables.DerivedBidiClass.txt, BidiMirroring.txt, …). That is your ground truth when a lookup surprises you.First real use case
You have a string from user input and need to know whether it is right-to-left before laying it out, truncating it, or choosing a font:
use Com\Tecnick\Unicode\Data\Pattern;
use Com\Tecnick\Unicode\Data\Type;
if (preg_match(Pattern::RTL, $text)) {
// contains right-to-left code points
}
// per-character resolution when you need to split
foreach (mb_str_split($text) as $char) {
$class = Type::getType(mb_ord($char, 'UTF-8'));
}
That is the whole day-one surface. If you find yourself wanting anything algorithmic — reordering, shaping, line breaking — that lives in tc-lib-unicode, not here.
Every class here is a bag of constants plus a handful of pure static lookups. There is no instance state, no config, no DI container hook. The pragmatic pattern is to wrap the calls you use in one thin service of your own, so your application code depends on an interface rather than a frozen static:
final class UnicodeClassifier
{
public function bidiClass(int $codepoint): ?string
{
return Type::getBidiClass($codepoint);
}
public function isRtl(string $text): bool
{
return $text !== '' && preg_match(Pattern::RTL, $text) === 1;
}
}
This is the single most valuable thing you can do with this package: it gives you a seam for tests and for swapping in intl or another data source later.
Pattern::RTL is a code-point-based regex (it carries the u modifier) that matches any run of characters in the right-to-left blocks. Use it as a cheap gate before doing anything expensive. Don't use it to determine paragraph direction — for that you need the first strong character, which means iterating with Type::getType() and stopping at the first L, R or AL:
foreach (mb_str_split($text) as $char) {
$t = Type::getType(mb_ord($char, 'UTF-8'));
if ($t === 'L') return 'ltr';
if ($t === 'R' || $t === 'AL') return 'rtl';
}
Arabic::getJoiningType() returns the UCD Joining_Type (D, R, L, C, T, U, …). This is the input to any letter-joining/shaping routine. The common workflow is to walk the string once, collect joining types into an array, then resolve each character's four presentation forms from Arabic::SUBSTITUTE:
$forms = Arabic::SUBSTITUTE[$codepoint] ?? null;
// always [isolated, final, initial, medial]
Because v3 guarantees exactly four entries per row (repeating isolated/final where the letter has no initial or medial form), you can index positionally without a null check — a genuine ergonomic improvement over 2.x.
A shaping or bidi pass touches the same code points repeatedly (spaces, digits, punctuation, ا, ل). Wrap lookups in an array cache:
private array $cache = [];
public function type(int $cp): ?string
{
return $this->cache[$cp] ??= Type::getType($cp);
}
The underlying call is cheap but not free, and long documents make the difference measurable.
If you are doing any parenthesis balancing or RTL-aware bracket rendering, Bracket gives you the canonical paired-bracket map (rule N0) and Mirror the mirroring glyphs (rule L4). Both are plain integer-keyed arrays: Bracket::UNI[$ord] and Mirror::UNI[$ord]. Use these instead of hand-rolling a [/] table — the UCD includes far more pairs than you'd expect, and v3 deliberately removed the six quotation marks that 2.x incorrectly mirrored.
The data changes with each Unicode release, and classification results can shift for individual code points. If your output is snapshotted (PDF fixtures, golden test files, search indexes), pin to a minor version and treat data upgrades as behaviour changes:
"tecnickcom/tc-lib-unicode-data": "~3.1.0"
Then upgrade deliberately, run your fixture diff, and only then bump.
When used via tc-lib-pdf, keep this package's version aligned with the versions tc-lib-pdf was tested against. Mixing a newer data package with an older tc-lib-unicode is the most common source of "why did my line break change" bug reports in this ecosystem. Let Composer resolve it and resist composer update tecnickcom/tc-lib-unicode-data --with-dependencies on its own.
The 2.x → 3.x change will silently break naive lookups. Type::UNI in v3 only lists code points whose Bidi_Class is not L. The old idiom:
$class = Type::UNI[$ord] ?? 'L'; // WRONG in 3.x
now misclassifies every formatting code (LRE, LRO, RLE, RLO, PDF, LRI, RLI, FSI, PDI — which return null) and every unassigned code point in blocks that default to R, AL or ET. Use Type::getType($ord) and let it handle the block defaults for you.
getBidiClass() returns null for exactly nine code points. The explicit formatting characters above. getType() gives L for everything else that isn't listed. If you write if ($class === 'L') on the result of getBidiClass(), you're comparing a constant, not a string — check what BidiClass::L actually holds and compare against that.
The regexes fatally require valid UTF-8. preg_match(Pattern::RTL, $text) returns false (not 0) on invalid UTF-8, and your if (preg_match(...)) branch is then skipped without a warning. Any text that has passed through user upload, a legacy database column, or a truncation routine can be invalid. Guard with mb_check_encoding($text, 'UTF-8') first, or at minimum distinguish === 1 from === false.
Large constant arrays and memory. The generated tables are big integer-keyed arrays. They are only materialised when the class is autoloaded, but if you touch Mirror::UNI and Type::UNI in the same request you pay for both. On constrained memory_limit hosts (shared hosting, small containers) this is a real ceiling — check with memory_get_peak_usage() before blaming your own code. Enabling OPcache with opcache.preload on the data classes removes most of the per-request cost; without OPcache the parsing of these files on every request is the dominant expense.
Everything is static, so nothing is mockable. There is no interface, no factory, no injection point. Unit tests that hit the real tables are effectively integration tests tied to a specific Unicode version. Either wrap the calls (see above) or accept that your tests pin a data version.
There are no extension points. The classes are data containers; you cannot subclass your way to a custom table or override a lookup. If you need data from a newer UCD release than the package ships, you either wait for a release or fork and run the generator yourself:
make gendata UCDVERSION=17.0.0
make qa
That requires make, a PHP toolchain and network access to the UCD files. For a one-off, it is far easier to cross-check a specific classification against the Unicode Character Database or PHP's intl extension (IntlChar::getIntPropertyValue) and work around it locally.
Debugging a classification. When a code point behaves unexpectedly, print both the resolved type and the block:
printf("U+%04X type=%s class=%s\n", $ord, Type::getType($ord), var_export(Type::getBidiClass($ord), true));
Then look the code point up on unicode.org against the version reported by Type::UNICODE_VERSION — not against the latest published UCD. Mismatched versions explain most "wrong" results.
License is LGPL-3.0, and Packagist reports it as NOASSERTION. The README and LICENSE file are explicit, but automated license scanners will flag this package as unknown. If you have a compliance gate, add a manual approval with the LGPL-3.0 reference; the practical obligation is that modifications to this library must be shared, while your application merely linking to it is generally fine.
Identity::CIDHMAP is not a Unicode table. It is the CID-to-glyph map used for CJK font subsetting. The README's quick-start hashes it purely as a smoke test that the autoloader works. Don't go looking for bidi data there.
Vendored/system installs need a different bootstrap. If you are not using Composer, the package ships its own autoloader:
require_once '/usr/share/php/Com/Tecnick/Unicode/Data/autoload.php';
This is common in RPM/DEB deployments (make rpm, make deb), where the normal vendor/autoload.php path does not exist. Missing this is the usual cause of "class not found" in packaged environments.
Don't iterate the constants as a source of truth. Type::UNI, Mirror::UNI, Arabic::END and friends are lookup structures. Building a full code-point inventory by iterating them will silently miss every code point that resolves via a default rule, and the array ordering is an artefact of generation, not something to depend on.
How can I help you explore Laravel packages today?