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
8 changes: 0 additions & 8 deletions src/OtlDump.php
Original file line number Diff line number Diff line change
Expand Up @@ -433,14 +433,6 @@ protected function gsubReverseChainRule(array $Lookup, $i, $c, $tag, $scripttag,
return [];
}

/**
* The dump reads each class-based rule's positions fresh, where the parser does not (#170).
*/
protected function keepsEarlierRulePositions()
{
return false;
}

/**
* One substitution, as a row of the report.
*
Expand Down
49 changes: 22 additions & 27 deletions src/TTFontFile.php
Original file line number Diff line number Diff line change
Expand Up @@ -2412,8 +2412,8 @@ function _getGSUBarray(array $Lookup, $lul, $scripttag)
{
$volt = [];

// Kept across rules and lookups rather than read fresh for each: see keepsEarlierRulePositions().
// A Type 5 Format 1 or 2 rule hands whatever these last held to the Arabic shaper (#189).
// A Type 5 Format 1 or 2 rule reads these without setting them, so hands whatever the last
// chained rule left in them to the Arabic shaper (#189).
$backtrackGlyphs = [];
$lookaheadGlyphs = [];

Expand Down Expand Up @@ -2544,19 +2544,7 @@ function _getGSUBarray(array $Lookup, $lul, $scripttag)
for ($cscrule = 0; $cscrule < $cscs['ChainSubClassRuleCnt']; $cscrule++) {
$rule = $cscs['ChainSubClassRule'][$cscrule];

if (!$rule['BacktrackGlyphCount'] || !$this->keepsEarlierRulePositions()) {
$backtrackGlyphs = [];
}
for ($gcl = 0; $gcl < $rule['BacktrackGlyphCount']; $gcl++) {
$backtrackGlyphs[$gcl] = $this->classGlyphs($subtable['BacktrackClasses'], $rule['Backtrack'][$gcl]);
}

if (!$rule['LookaheadGlyphCount'] || !$this->keepsEarlierRulePositions()) {
$lookaheadGlyphs = [];
}
for ($gcl = 0; $gcl < $rule['LookaheadGlyphCount']; $gcl++) {
$lookaheadGlyphs[$gcl] = $this->classGlyphs($subtable['LookaheadClasses'], $rule['Lookahead'][$gcl]);
}
list($backtrackGlyphs, $lookaheadGlyphs) = $this->classSequences($subtable, $rule);

$this->addTo($volt, $this->gsubContextRule($Lookup, $i, $c, $tag, $scripttag, $ignore, $this->contextRule(
$cscrule,
Expand Down Expand Up @@ -2638,6 +2626,25 @@ private function coverageSequences(array $subtable)
];
}

/**
* @return array [backtrack, lookahead] of one class-based chained rule (Type 6 Format 2), one
* "|"-joined glyph string per position
*/
private function classSequences(array $subtable, array $rule)
{
$backtrack = [];
for ($gcl = 0; $gcl < $rule['BacktrackGlyphCount']; $gcl++) {
$backtrack[$gcl] = $this->classGlyphs($subtable['BacktrackClasses'], $rule['Backtrack'][$gcl]);
}

$lookahead = [];
for ($gcl = 0; $gcl < $rule['LookaheadGlyphCount']; $gcl++) {
$lookahead[$gcl] = $this->classGlyphs($subtable['LookaheadClasses'], $rule['Lookahead'][$gcl]);
}

return [$backtrack, $lookahead];
}

/**
* The input sequence of a class-based rule: the class its rule set is for, then the class of each
* position after the first.
Expand Down Expand Up @@ -2679,18 +2686,6 @@ private function addTo(array &$volt, array $entries)
}
}

/**
* Whether a class-based chained rule (Type 6 Format 2) that names fewer backtrack or lookahead
* positions than the rule read before it keeps that rule's extra positions.
*
* The parser does, which is #170: the extra positions end up in the rule's matchback and counts,
* and in what an Arabic joining form hands the shaper. Fixing it is returning false here.
*/
protected function keepsEarlierRulePositions()
{
return true;
}

/**
* The rules of a Single, Multiple, Alternate or Ligature substitution subtable (Types 1 to 4).
*
Expand Down
82 changes: 82 additions & 0 deletions tests/Mpdf/ChainedClassRulePositionsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

namespace Mpdf;

/**
* Each rule of a class-based chained context (GSUB Type 6 Format 2) matches its own backtrack and
* lookahead positions, and none the rule before it named (#170).
*
* No font in the corpus has a rule naming fewer positions than the one before it in a joining form's
* lookup, which is where the shaper sees the difference. NotoSansArabic-GSUB62Positions-Synthetic is
* NotoSansArabic-Joining-Subset (Noto Sans Arabic 2.012, OFL 1.1) with its GSUB replaced by one
* 'fina' lookup of that type. Alef's rule gives alef its final form after beh then alef and before
* two low alefs; beh's rule, read after it, gives beh the dotless final form after one beh and before
* one low alef. `hb-shape` draws the glyphs these tests expect.
*/
class ChainedClassRulePositionsTest extends \Yoast\PHPUnitPolyfills\TestCases\TestCase
{

const ALEF = 0x0627;

const BEH = 0x0628;

const LOW_ALEF = 0x08AD;

/** The final forms have no codepoint, so they are mapped into the Private Use Area */
const ALEF_FINAL = 0xE000;

const DOTLESS_BEH_FINAL = 0xE002;

/**
* @param int[] $codepoints
*
* @return int[] the codepoints of the line as it is handed to the drawing code, in visual order
*/
private function drawn($codepoints)
{
$html = '';
foreach ($codepoints as $codepoint) {
$html .= sprintf('&#x%04X;', $codepoint);
}

$mpdf = new TextRecordingMpdf([
'mode' => 'utf-8',
'fontDir' => [__DIR__ . '/../data/ttf'],
'fontdata' => ['notosansarabicgsub62positionssynthetic' => [
'R' => 'NotoSansArabic-GSUB62Positions-Synthetic.ttf',
'useOTL' => 0xFF,
]],
'default_font' => 'notosansarabicgsub62positionssynthetic',
]);
$mpdf->WriteHTML('<p>' . $html . '</p>');

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

public function dataRuns()
{
return [
'the first rule, with two positions either side' => [
[self::ALEF, self::BEH, self::ALEF, self::LOW_ALEF, self::LOW_ALEF],
[self::LOW_ALEF, self::LOW_ALEF, self::ALEF_FINAL, self::BEH, self::ALEF],
],
'the second rule, where the first rule\'s second backtrack position does not hold' => [
[self::BEH, self::BEH, self::LOW_ALEF, self::LOW_ALEF],
[self::LOW_ALEF, self::LOW_ALEF, self::DOTLESS_BEH_FINAL, self::BEH],
],
'the second rule, where the first rule\'s second lookahead position does not hold' => [
[self::ALEF, self::BEH, self::BEH, self::LOW_ALEF],
[self::LOW_ALEF, self::DOTLESS_BEH_FINAL, self::BEH, self::ALEF],
],
];
}

/**
* @dataProvider dataRuns
*/
public function testEachRuleMatchesItsOwnPositions($codepoints, $expected)
{
$this->assertSame($expected, $this->drawn($codepoints));
}

}
20 changes: 9 additions & 11 deletions tests/Mpdf/Fonts/GsubArrayTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@
use Mpdf\TTFontFile;

/**
* The GSUB walk the parser and the dump share, where the two part ways on purpose and no font in
* tests/data/ttf shows it.
* The GSUB walk the parser and the dump share, over rule sets no font in tests/data/ttf has.
*
* The lookups are built by hand, in the shape readGSUBrules() leaves them: lookup 0 is a class-based
* chained context (Type 6 Format 2) whose two rules name first two backtrack positions, then one, and
Expand All @@ -20,21 +19,21 @@ class GsubArrayTest extends \Yoast\PHPUnitPolyfills\TestCases\TestCase
{

/**
* The parser keeps the first rule's second backtrack position into the second rule (#170).
* The second rule matches the one backtrack position it names, not the first rule's second as
* well (#170).
*/
public function testTheParserKeepsAnEarlierClassRulesExtraPositions()
public function testTheParserReadsEachClassRuleWithItsOwnPositions()
{
$parser = $this->withGdef(new TTFontFile($this->cache(), 'win'));

$volt = $parser->_getGSUBarray($this->chainedClassRules(), [0 => 'ccmp'], 'latn');

$this->assertSame([2, 2], [$volt[0]['nBacktrack'], $volt[1]['nBacktrack']]);
$this->assertSame('(00043)() (00043)() ', $volt[1]['matchback']);
$this->assertSame([2, 1], [$volt[0]['nBacktrack'], $volt[1]['nBacktrack']]);
$this->assertSame('(00043)() ', $volt[1]['matchback']);
}

/**
* The dump reads each rule's positions fresh, so it reports the second rule with the one
* backtrack position it names.
* So does the dump, which reports the second rule with the one backtrack position it names.
*/
public function testTheDumpReportsEachClassRuleWithItsOwnPositions()
{
Expand All @@ -56,8 +55,7 @@ public function testTheDumpReportsEachClassRuleWithItsOwnPositions()

/**
* A Type 5 rule has no backtrack or lookahead, yet where it belongs to an Arabic joining form the
* parser hands the shaper the sequences of the chained rule it read last. Kept as it was by the
* walk the two share; see #189.
* parser hands the shaper the sequences of the chained rule it read last (#189).
*/
public function testAPlainContextRuleOfAnArabicFormCarriesTheLastChainedRulesSequences()
{
Expand All @@ -80,7 +78,7 @@ public function testAPlainContextRuleOfAnArabicFormCarriesTheLastChainedRulesSeq

$volt = $parser->_getGSUBarray($Lookup, [0 => 'init', 2 => 'init'], 'arab');

$this->assertSame(['match' => '00041', 'replace' => '00044', 'tag' => 'init', 'prel' => ['00043', '00043'], 'postl' => [], 'ignore' => '()'], end($volt));
$this->assertSame(['match' => '00041', 'replace' => '00044', 'tag' => 'init', 'prel' => ['00043'], 'postl' => [], 'ignore' => '()'], end($volt));
}

private function chainedClassRules()
Expand Down
Loading
Loading