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
22 changes: 11 additions & 11 deletions src/Otl.php
Original file line number Diff line number Diff line change
Expand Up @@ -1642,13 +1642,13 @@ function _applyGSUBsubtableSpecial($lookupID, $subtable, $ptr, $currGlyph, $curr
////////////////////////////////////////////////////////////////////////////////
$Coverage = $subtable_offset + $this->reader->readUInt16();
$NextGlyphPos = $LuCoverage[$nextGID];
$LigSetCount = $this->reader->readInt16();
$LigSetCount = $this->reader->readUInt16();

$this->reader->skip($NextGlyphPos * 2);
$LigSet = $subtable_offset + $this->reader->readInt16();
$LigSet = $subtable_offset + $this->reader->readUInt16();

$this->reader->seek($LigSet);
$LigCount = $this->reader->readInt16();
$LigCount = $this->reader->readUInt16();
// LigatureSet i.e. all starting with the same Glyph $nextGlyph [Consonant]
$LigatureOffset = [];
for ($g = 0; $g < $LigCount; $g++) {
Expand Down Expand Up @@ -1824,10 +1824,10 @@ private function _applyGSUBmultipleSubst($lookupID, $subtable, $ptr, $currGlyph,
$GlyphPos = $LuCoverage[$currGID];
$this->reader->skip(2);
$this->reader->skip($GlyphPos * 2);
$Sequences = $subtable_offset + $this->reader->readInt16();
$Sequences = $subtable_offset + $this->reader->readUInt16();

$this->reader->seek($Sequences);
$GlyphCount = $this->reader->readInt16();
$GlyphCount = $this->reader->readUInt16();
$SubstituteGlyphs = [];
for ($g = 0; $g < $GlyphCount; $g++) {
$sgid = $this->reader->readUInt16();
Expand Down Expand Up @@ -1861,7 +1861,7 @@ private function _applyGSUBalternateSubst($lookupID, $subtable, $ptr, $currGlyph
return 0;
}
$Coverage = $subtable_offset + $this->reader->readUInt16();
$AlternateSetCount = $this->reader->readInt16();
$AlternateSetCount = $this->reader->readUInt16();
///////////////////////////////////////////////////////////////////////////////!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// Need to set alternate IF set by CSS3 font-feature for a tag
// i.e. if this is 'salt' alternate may be set to 2
Expand All @@ -1878,10 +1878,10 @@ private function _applyGSUBalternateSubst($lookupID, $subtable, $ptr, $currGlyph
$GlyphPos = $LuCoverage[$currGID];
$this->reader->skip($GlyphPos * 2);

$AlternateSets = $subtable_offset + $this->reader->readInt16();
$AlternateSets = $subtable_offset + $this->reader->readUInt16();
$this->reader->seek($AlternateSets);

$AlternateGlyphCount = $this->reader->readInt16();
$AlternateGlyphCount = $this->reader->readUInt16();
if ($alt > $AlternateGlyphCount) {
return 0;
} // If specified alternate not present, cancel [ or could default $alt = 1 ?]
Expand Down Expand Up @@ -1919,13 +1919,13 @@ private function _applyGSUBligatureSubst($lookupID, $subtable, $ptr, $currGlyph,
$Coverage = $subtable_offset + $this->reader->readUInt16();
$FirstGlyphPos = $LuCoverage[$currGID];

$LigSetCount = $this->reader->readInt16();
$LigSetCount = $this->reader->readUInt16();

$this->reader->skip($FirstGlyphPos * 2);
$LigSet = $subtable_offset + $this->reader->readInt16();
$LigSet = $subtable_offset + $this->reader->readUInt16();

$this->reader->seek($LigSet);
$LigCount = $this->reader->readInt16();
$LigCount = $this->reader->readUInt16();
// LigatureSet i.e. all starting with the same first Glyph $currGlyph
$LigatureOffset = [];
for ($g = 0; $g < $LigCount; $g++) {
Expand Down
24 changes: 12 additions & 12 deletions src/OtlDump.php
Original file line number Diff line number Diff line change
Expand Up @@ -1205,14 +1205,14 @@ function _getGSUBtables()
else {
if ($Lookup[$i]['Type'] == 2) {
$Lookup[$i]['Subtable'][$c]['CoverageTableOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->reader->readUInt16();
$Lookup[$i]['Subtable'][$c]['SequenceCount'] = $SequenceCount = $this->reader->readInt16();
$Lookup[$i]['Subtable'][$c]['SequenceCount'] = $SequenceCount = $this->reader->readUInt16();
for ($s = 0; $s < $SequenceCount; $s++) {
$Lookup[$i]['Subtable'][$c]['Sequences'][$s]['Offset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->reader->readInt16();
$Lookup[$i]['Subtable'][$c]['Sequences'][$s]['Offset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->reader->readUInt16();
}
for ($s = 0; $s < $SequenceCount; $s++) {
// Sequence Tables
$this->reader->seek($Lookup[$i]['Subtable'][$c]['Sequences'][$s]['Offset']);
$Lookup[$i]['Subtable'][$c]['Sequences'][$s]['GlyphCount'] = $this->reader->readInt16();
$Lookup[$i]['Subtable'][$c]['Sequences'][$s]['GlyphCount'] = $this->reader->readUInt16();
for ($g = 0; $g < $Lookup[$i]['Subtable'][$c]['Sequences'][$s]['GlyphCount']; $g++) {
$Lookup[$i]['Subtable'][$c]['Sequences'][$s]['SubstituteGlyphID'][] = $this->reader->readUInt16();
}
Expand All @@ -1221,15 +1221,15 @@ function _getGSUBtables()
else {
if ($Lookup[$i]['Type'] == 3) {
$Lookup[$i]['Subtable'][$c]['CoverageTableOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->reader->readUInt16();
$Lookup[$i]['Subtable'][$c]['AlternateSetCount'] = $AlternateSetCount = $this->reader->readInt16();
$Lookup[$i]['Subtable'][$c]['AlternateSetCount'] = $AlternateSetCount = $this->reader->readUInt16();
for ($s = 0; $s < $AlternateSetCount; $s++) {
$Lookup[$i]['Subtable'][$c]['AlternateSets'][$s]['Offset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->reader->readInt16();
$Lookup[$i]['Subtable'][$c]['AlternateSets'][$s]['Offset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->reader->readUInt16();
}

for ($s = 0; $s < $AlternateSetCount; $s++) {
// AlternateSet Tables
$this->reader->seek($Lookup[$i]['Subtable'][$c]['AlternateSets'][$s]['Offset']);
$Lookup[$i]['Subtable'][$c]['AlternateSets'][$s]['GlyphCount'] = $this->reader->readInt16();
$Lookup[$i]['Subtable'][$c]['AlternateSets'][$s]['GlyphCount'] = $this->reader->readUInt16();
for ($g = 0; $g < $Lookup[$i]['Subtable'][$c]['AlternateSets'][$s]['GlyphCount']; $g++) {
$Lookup[$i]['Subtable'][$c]['AlternateSets'][$s]['SubstituteGlyphID'][] = $this->reader->readUInt16();
}
Expand All @@ -1238,14 +1238,14 @@ function _getGSUBtables()
else {
if ($Lookup[$i]['Type'] == 4) {
$Lookup[$i]['Subtable'][$c]['CoverageTableOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->reader->readUInt16();
$Lookup[$i]['Subtable'][$c]['LigSetCount'] = $LigSetCount = $this->reader->readInt16();
$Lookup[$i]['Subtable'][$c]['LigSetCount'] = $LigSetCount = $this->reader->readUInt16();
for ($s = 0; $s < $LigSetCount; $s++) {
$Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Offset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->reader->readInt16();
$Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Offset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->reader->readUInt16();
}
for ($s = 0; $s < $LigSetCount; $s++) {
// LigatureSet Tables
$this->reader->seek($Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Offset']);
$Lookup[$i]['Subtable'][$c]['LigSet'][$s]['LigCount'] = $this->reader->readInt16();
$Lookup[$i]['Subtable'][$c]['LigSet'][$s]['LigCount'] = $this->reader->readUInt16();
for ($g = 0; $g < $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['LigCount']; $g++) {
$Lookup[$i]['Subtable'][$c]['LigSet'][$s]['LigatureOffset'][$g] = $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Offset'] + $this->reader->readUInt16();
}
Expand All @@ -1267,14 +1267,14 @@ function _getGSUBtables()
// Format 1: Context Substitution
if ($SubstFormat == 1) {
$Lookup[$i]['Subtable'][$c]['CoverageTableOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->reader->readUInt16();
$Lookup[$i]['Subtable'][$c]['SubRuleSetCount'] = $SubRuleSetCount = $this->reader->readInt16();
$Lookup[$i]['Subtable'][$c]['SubRuleSetCount'] = $SubRuleSetCount = $this->reader->readUInt16();
for ($s = 0; $s < $SubRuleSetCount; $s++) {
$Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['Offset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->reader->readInt16();
$Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['Offset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->reader->readUInt16();
}
for ($s = 0; $s < $SubRuleSetCount; $s++) {
// SubRuleSet Tables
$this->reader->seek($Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['Offset']);
$Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRuleCount'] = $this->reader->readInt16();
$Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRuleCount'] = $this->reader->readUInt16();
for ($g = 0; $g < $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRuleCount']; $g++) {
$Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRuleOffset'][$g] = $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['Offset'] + $this->reader->readUInt16();
}
Expand Down
126 changes: 126 additions & 0 deletions tests/Mpdf/LargeSubtableTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<?php

namespace Mpdf;

/**
* A GSUB subtable is reached from the lookup that holds it by an Offset16, and the arrays inside the
* three substitution types that hold a record per covered glyph - Multiple, Alternate and Ligature -
* are Offset16 as well. Offset16 is unsigned, so a subtable larger than 32,767 bytes has entries at
* or past 0x8000.
*
* The shaper read those arrays with a signed reader, so an entry past the boundary came back negative
* and it seeked 65,536 bytes short of the record it wanted, decoding whatever the table holds there:
* another glyph's replacement, and a glyph count belonging to neither.
*
* NotoSans-GSUB2-BigSubtable-Synthetic is built for this. It is 1,000 covered glyphs, each replaced
* by a run of fifteen markers with the glyph itself in the middle - the shape of a font that shapes
* by expansion, computes on the markers and collapses the run again. The Sequence array is 36,072
* bytes, so the last 95 entries sit past 0x8000. Only the glyphs a test types carry an outline.
*/
class LargeSubtableTest extends \Yoast\PHPUnitPolyfills\TestCases\TestCase
{

/** The first covered glyph, whose Sequence is 2,006 bytes into the subtable */
const EARLY = 0xE000;

/** The first covered glyph whose Sequence sits at or past 0x8000, at 32,776 bytes */
const FIRST_PAST_BOUNDARY = 0xE389;

/** The last covered glyph, whose Sequence is 35,982 bytes in */
const LAST = 0xE3E7;

/** Glyphs in the run each covered glyph is replaced by */
const SEQUENCE_LENGTH = 16;

/** Where the covered glyph itself sits in that run */
const SELF_POSITION = 8;

/**
* @param int[] $codepoints
*
* @return int[] the codepoints of the line as it is handed to the drawing code
*/
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' => ['bigsubtable' => [
'R' => 'NotoSans-GSUB2-BigSubtable-Synthetic.ttf',
'useOTL' => 0xFF,
]],
'default_font' => 'bigsubtable',
]);
$mpdf->WriteHTML('<p>' . $html . '</p>');

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

/**
* The control: a glyph whose Sequence is well inside the first 32 KB was always read correctly.
* The markers have no codepoints of their own, so the run is read by its shape rather than by
* naming them.
*/
public function testAGlyphBeforeTheBoundaryExpandsIntoItsOwnRun()
{
$drawn = $this->drawn([self::EARLY]);

$this->assertCount(self::SEQUENCE_LENGTH, $drawn);
$this->assertSame(self::EARLY, $drawn[self::SELF_POSITION]);
}

/**
* The first glyph past the boundary. Read signed, its offset was negative: the shaper seeked
* before the subtable, read a glyph count of its own and replaced the character with eight glyphs
* belonging to another - the character typed was not on the page at all.
*/
public function testAGlyphPastTheBoundaryExpandsIntoItsOwnRun()
{
$drawn = $this->drawn([self::FIRST_PAST_BOUNDARY]);

$this->assertCount(self::SEQUENCE_LENGTH, $drawn);
$this->assertSame(self::FIRST_PAST_BOUNDARY, $drawn[self::SELF_POSITION]);
}

/**
* Every covered glyph is given the same markers, so the two runs have to agree everywhere but the
* middle. Run against the last glyph of the subtable, 3 KB further past the boundary, so that a
* glyph past it is read from its own Sequence rather than from one that happens to be as long.
*/
public function testTheRunPastTheBoundaryIsTheSameRunAsTheOneBeforeIt()
{
$early = $this->drawn([self::EARLY]);
$late = $this->drawn([self::LAST]);

unset($early[self::SELF_POSITION], $late[self::SELF_POSITION]);

$this->assertSame($early, $late);
}

/**
* Four of them in one run. A font that shapes this way feeds the markers to chaining rules that
* compute on them, so a Sequence read from the wrong place is not one wrong glyph but a run those
* rules then match against and expand again: Noto Sans Duployan turned four characters into
* 102,109 glyphs and 52 seconds of shaping.
*/
public function testEachGlyphOfARunExpandsOnce()
{
$run = [self::LAST - 3, self::LAST - 2, self::LAST - 1, self::LAST];
$drawn = $this->drawn($run);

$this->assertCount(count($run) * self::SEQUENCE_LENGTH, $drawn);
foreach ($run as $i => $codepoint) {
$this->assertSame(
$codepoint,
$drawn[$i * self::SEQUENCE_LENGTH + self::SELF_POSITION],
sprintf('the character at position %d is not in the run it expanded into', $i)
);
}
}

}
Loading
Loading