Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 55 additions & 21 deletions src/Shaper/OtlTags.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ class OtlTags
'mym2' => 'mymr',
];

/**
* The Chinese regions with language systems of their own, in the order HarfBuzz looks for them.
*/
private static $chineseRegions = [
'hk' => ['ZHH '],
'mo' => ['ZHTM', 'ZHH '],
'tw' => ['ZHT '],
];

/**
* The script tag a run is laid out with, out of what the table offers.
*
Expand Down Expand Up @@ -77,43 +86,68 @@ public static function script(array $ScriptLang, $scripttag, $scriptblock, $shap
* @param string $ietf The language of the text as an IETF tag, e.g. 'sr-Cyrl' or 'zh-Hant-HK'
* @param string $available The language systems the script offers, as one string of tags
*
* @return string The tag, 'DFLT' where the script offers that and not the language, or '' for neither
* @return string The first of the language's tags the script offers, 'DFLT' where it offers none of
* them and offers that, or '' for neither
*/
public static function language($ietf, $available)
{
if ($available == '') {
return '';
}

$tags = $ietf ? explode('-', $ietf) : [];

$lang = isset($tags[0]) ? strtolower($tags[0]) : '';
$subtags = $ietf ? explode('-', strtolower($ietf)) : [];

// The region, where the second subtag is one; a third subtag always is, the second being
// the script
$country = '';
if (isset($tags[1]) && strlen($tags[1]) == 2) {
$country = strtolower($tags[1]);
$candidates = [];
if (isset($subtags[0]) && $subtags[0] == 'zh') {
$candidates = self::chinese($subtags);
} elseif (isset($subtags[0]) && isset(Ucdn::$ot_languages[$subtags[0]])) {
$candidates = [Ucdn::$ot_languages[$subtags[0]]];
}
if (isset($tags[2]) && $tags[2]) {
$country = strtolower($tags[2]);

foreach ($candidates as $langsys) {
if (strpos($available, $langsys) !== false) {
return $langsys;
}
}

$region = $lang . '-' . $country;
return strpos($available, 'DFLT') !== false ? 'DFLT' : '';
}

if ($lang != '' && isset(Ucdn::$ot_languages[$lang])) {
$langsys = Ucdn::$ot_languages[$lang];
} elseif ($lang != '' && $country != '' && isset(Ucdn::$ot_languages[$region])) {
$langsys = Ucdn::$ot_languages[$region];
} else {
$langsys = 'DFLT';
/**
* The language systems for Chinese, in the order HarfBuzz tries them
* (hb_ot_tags_from_complex_language() in hb-ot-tag-table.hh).
*
* The script subtag outranks the region, so zh-Hans-HK is Simplified, except that Traditional
* Chinese in Hong Kong or Macao keeps its region's tag. Macao has its own ZHTM, and where a font
* lacks it takes Hong Kong's ZHH before the default. A tag with neither is Simplified.
*
* The zh- keys of Ucdn::$ot_languages are not read: they hold one tag per region, and nothing for
* the script or for zh alone.
*
* @param string[] $subtags The lower-cased subtags of a tag whose language is zh
*
* @return string[]
*/
private static function chinese(array $subtags)
{
$script = isset($subtags[1]) ? $subtags[1] : '';

if ($script == 'hans') {
return ['ZHS '];
}
if ($script == 'hant') {
$region = isset($subtags[2]) ? $subtags[2] : '';

if (strpos($available, $langsys) !== false) {
return $langsys;
return $region == 'hk' || $region == 'mo' ? self::$chineseRegions[$region] : ['ZHT '];
}

return strpos($available, 'DFLT') !== false ? 'DFLT' : '';
foreach (self::$chineseRegions as $region => $langsys) {
if (in_array($region, $subtags, true)) {
return $langsys;
}
}

return ['ZHS '];
}

/**
Expand Down
74 changes: 74 additions & 0 deletions tests/Mpdf/ChineseLangSysTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

namespace Mpdf;

/**
* The lang attribute selects the font's Chinese language system the way HarfBuzz does: the script
* subtag before the region, ZHS for zh alone, and for Macao ZHTM, then ZHH.
*
* No font in the corpus has a Chinese language system, so both fonts here are subsets of Noto Sans TC
* 2.004 (OFL 1.1) with their GSUB replaced. Their hani script has no features by default, and one
* 'locl' lookup per language system giving 骨 the glyph of 三 under ZHH, 一 under ZHS and 二 under
* ZHT. NotoSansTC-MacaoLangSys-Synthetic also has 四 under ZHTM. `hb-shape --language` draws the
* same glyph for every case below.
*/
class ChineseLangSysTest extends \Yoast\PHPUnitPolyfills\TestCases\TestCase
{

const BONE = 0x9AA8;

const ZHS = 0x4E00;

const ZHT = 0x4E8C;

const ZHH = 0x4E09;

const ZHTM = 0x56DB;

/**
* @dataProvider languages
*/
public function testTheLanguageSelectsTheLanguageSystem($lang, $expected)
{
$this->assertSame([$expected], $this->drawn('NotoSansTC-RegionLangSys-Synthetic.ttf', $lang));
}

public function languages()
{
return [
'zh-HK' => ['zh-HK', self::ZHH],
'zh-TW' => ['zh-TW', self::ZHT],
'zh-CN' => ['zh-CN', self::ZHS],
'zh' => ['zh', self::ZHS],
'zh-Hans' => ['zh-Hans', self::ZHS],
'zh-Hant' => ['zh-Hant', self::ZHT],
'zh-Hans-HK' => ['zh-Hans-HK', self::ZHS],
'zh-Hant-CN' => ['zh-Hant-CN', self::ZHT],
'zh-MO, where the font has no ZHTM' => ['zh-MO', self::ZHH],
];
}

public function testMacaoSelectsZHTMWhereTheFontOffersIt()
{
$this->assertSame([self::ZHTM], $this->drawn('NotoSansTC-MacaoLangSys-Synthetic.ttf', 'zh-MO'));
}

private function drawn($font, $lang)
{
$key = strtolower(str_replace('-', '', basename($font, '.ttf')));

$mpdf = new TextRecordingMpdf([
'mode' => 'utf-8',
'fontDir' => [__DIR__ . '/../data/ttf'],
'fontdata' => [$key => [
'R' => $font,
'useOTL' => 0xFF,
]],
'default_font' => $key,
]);
$mpdf->WriteHTML(sprintf('<p lang="%s">&#x%04X;</p>', $lang, self::BONE));

return array_values(unpack('N*', mb_convert_encoding($mpdf->drawnText[0], 'UTF-32BE', 'UTF-8')));
}

}
55 changes: 0 additions & 55 deletions tests/Mpdf/ChineseRegionLangSysTest.php

This file was deleted.

38 changes: 23 additions & 15 deletions tests/Mpdf/Shaper/OtlTagsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,36 +124,44 @@ public function languages()
}

/**
* @dataProvider chineseRegions
* @dataProvider chinese
*/
public function testAChineseRegionSelectsItsLanguageSystem($ietf, $expected)
public function testChineseSelectsItsLanguageSystemTheWayHarfBuzzDoes($ietf, $expected)
{
$this->assertSame($expected, OtlTags::language($ietf, 'DFLT ZHH ZHS ZHT '));
$this->assertSame($expected, OtlTags::language($ietf, 'DFLT ZHH ZHS ZHT ZHTM '));
}

public function chineseRegions()
public function chinese()
{
return [
'Hong Kong' => ['zh-HK', 'ZHH '],
'Hong Kong, after the script' => ['zh-Hant-HK', 'ZHH '],
'Taiwan' => ['zh-TW', 'ZHT '],
'Macao' => ['zh-MO', 'ZHT '],
'Macao' => ['zh-MO', 'ZHTM'],
'China' => ['zh-CN', 'ZHS '],
'Singapore' => ['zh-SG', 'ZHS '],
'lower case' => ['zh-tw', 'ZHT '],
'a region the table has no entry for' => ['zh-US', 'DFLT'],
'a region with no language system of its own' => ['zh-US', 'ZHS '],
'no script or region' => ['zh', 'ZHS '],
'Simplified' => ['zh-Hans', 'ZHS '],
'Traditional' => ['zh-Hant', 'ZHT '],
'upper case' => ['ZH-HANT', 'ZHT '],
'Simplified, in Hong Kong' => ['zh-Hans-HK', 'ZHS '],
'Simplified, in Taiwan' => ['zh-Hans-TW', 'ZHS '],
'Simplified, in Macao' => ['zh-Hans-MO', 'ZHS '],
'Traditional, in China' => ['zh-Hant-CN', 'ZHT '],
'Traditional, in Taiwan' => ['zh-Hant-TW', 'ZHT '],
'Traditional, in Hong Kong' => ['zh-Hant-HK', 'ZHH '],
'Traditional, in Macao' => ['zh-Hant-MO', 'ZHTM'],
'another script, in Hong Kong' => ['zh-Latn-HK', 'ZHH '],
'Min Nan, a retired tag' => ['zh-min-nan', 'ZHS '],
];
}

/**
* Pins #201 as it stands: without a region, Chinese has no language system, where HarfBuzz
* takes ZHS for zh and zh-Hans and ZHT for zh-Hant.
*/
public function testChineseWithoutARegionHasNoLanguageSystem()
public function testMacaoFallsBackToHongKong()
{
$this->assertSame('DFLT', OtlTags::language('zh', 'DFLT ZHH ZHS ZHT '));
$this->assertSame('DFLT', OtlTags::language('zh-Hant', 'DFLT ZHH ZHS ZHT '));
$this->assertSame('DFLT', OtlTags::language('zh-Hans', 'DFLT ZHH ZHS ZHT '));
$this->assertSame('ZHH ', OtlTags::language('zh-MO', 'DFLT ZHH ZHS ZHT '));
$this->assertSame('ZHH ', OtlTags::language('zh-Hant-MO', 'DFLT ZHH ZHS ZHT '));
$this->assertSame('DFLT', OtlTags::language('zh-MO', 'DFLT ZHS ZHT '));
}

}
Loading
Loading