diff --git a/src/Otl.php b/src/Otl.php index 3a458afac..21e8ef9d7 100644 --- a/src/Otl.php +++ b/src/Otl.php @@ -4474,6 +4474,33 @@ private function _getMarkRecord($offset, $MarkPos) return $MarkRecord; } + /** + * UseMarkFilteringSet means "skip every mark except those in the given mark glyph set", so the glyphs to + * ignore are GlyphClassMarks minus that set - not the set itself. + * + * @param string $marks Space-prefixed, "|"-separated glyph list, e.g. " 00DCA| 00DD2" + * @param string $set The mark glyph set, in the same format + * + * @return string + */ + private function marksOutsideFilteringSet($marks, $set) + { + $keep = []; + $inSet = []; + foreach (explode('|', $set) as $glyph) { + $inSet[trim($glyph)] = true; + } + + foreach (explode('|', $marks) as $glyph) { + $glyph = trim($glyph); + if ($glyph !== '' && !isset($inSet[$glyph])) { + $keep[] = $glyph; + } + } + + return $keep ? ' ' . implode('| ', $keep) : ''; + } + private function _getGCOMignoreString($flag, $MarkFilteringSet) { // If ignoreFlag set, combine all ignore glyphs into -> "(?:( 0FBA1| 0FBA2| 0FBA3)*)" @@ -4496,7 +4523,8 @@ private function _getGCOMignoreString($flag, $MarkFilteringSet) if ($MarkFilteringSet === '' || !isset($this->MarkGlyphSets[$MarkFilteringSet])) { throw new \Mpdf\MpdfException("This font [" . $this->fontkey . "] contains MarkGlyphSets - but MarkFilteringSet not set"); } - $str = $this->MarkGlyphSets[$MarkFilteringSet]; + $ignoreflag = $flag; + $str = $this->marksOutsideFilteringSet($this->GlyphClassMarks, $this->MarkGlyphSets[$MarkFilteringSet]); } // If Ignore Marks set, supercedes any above @@ -4550,8 +4578,9 @@ private function _checkGCOMignore($flag, $glyph, $MarkFilteringSet) $ignore = true; } } - // Flag & 0x0010 = UseMarkFilteringSet - if (($flag & 0x0010) && strpos($this->MarkGlyphSets[$MarkFilteringSet], $glyph)) { + // Flag & 0x0010 = UseMarkFilteringSet: skip every mark *except* those in the set + if (($flag & 0x0010) && strpos($this->GlyphClassMarks, $glyph) + && !strpos($this->MarkGlyphSets[$MarkFilteringSet], $glyph)) { $ignore = true; } return $ignore; diff --git a/src/TTFontFile.php b/src/TTFontFile.php index 3da167fb8..0c64db88a 100644 --- a/src/TTFontFile.php +++ b/src/TTFontFile.php @@ -1344,7 +1344,8 @@ function _getGDEFtables() $MarkSetOffset[] = $this->read_ulong(); } for ($i = 0; $i < $MarkSetCount; $i++) { - $this->seek($MarkSetOffset[$i]); + // Coverage offsets are relative to the MarkGlyphSetsDef table, not the file + $this->seek($gdef_offset + $MarkGlyphSetsDef_offset + $MarkSetOffset[$i]); $glyphs = $this->_getCoverage(); $this->MarkGlyphSets[$i] = ' ' . implode('| ', $glyphs); } @@ -2441,6 +2442,10 @@ function _getGSUBarray(&$Lookup, &$lul, $scripttag) } } // LookupType 2: Multiple Substitution Subtable elseif ($Lookup[$i]['Type'] == 2) { + if (!isset($Lookup[$i]['Subtable'][$c]['subs'])) { + continue; // every entry was filtered out by the Ignore flags + } + for ($s = 0; $s < count($Lookup[$i]['Subtable'][$c]['subs']); $s++) { $inputGlyphs = $Lookup[$i]['Subtable'][$c]['subs'][$s]['Replace']; $substitute = implode(" ", $Lookup[$i]['Subtable'][$c]['subs'][$s]['substitute']); @@ -2451,6 +2456,10 @@ function _getGSUBarray(&$Lookup, &$lul, $scripttag) } } // LookupType 3: Alternate Forms elseif ($Lookup[$i]['Type'] == 3) { + if (!isset($Lookup[$i]['Subtable'][$c]['subs'])) { + continue; // every entry was filtered out by the Ignore flags + } + for ($s = 0; $s < count($Lookup[$i]['Subtable'][$c]['subs']); $s++) { $inputGlyphs = $Lookup[$i]['Subtable'][$c]['subs'][$s]['Replace']; $substitute = $Lookup[$i]['Subtable'][$c]['subs'][$s]['substitute'][0]; @@ -2461,6 +2470,10 @@ function _getGSUBarray(&$Lookup, &$lul, $scripttag) } } // LookupType 4: Ligature Substitution Subtable elseif ($Lookup[$i]['Type'] == 4) { + if (!isset($Lookup[$i]['Subtable'][$c]['subs'])) { + continue; // every entry was filtered out by the Ignore flags + } + for ($s = 0; $s < count($Lookup[$i]['Subtable'][$c]['subs']); $s++) { $inputGlyphs = $Lookup[$i]['Subtable'][$c]['subs'][$s]['Replace']; $substitute = $Lookup[$i]['Subtable'][$c]['subs'][$s]['substitute'][0]; @@ -2497,7 +2510,7 @@ function _getGSUBarray(&$Lookup, &$lul, $scripttag) // $Lookup[$lup] = secondary Lookup for ($lus = 0; $lus < $Lookup[$lup]['SubtableCount']; $lus++) { - if (count($Lookup[$lup]['Subtable'][$lus]['subs'])) { + if (!empty($Lookup[$lup]['Subtable'][$lus]['subs'])) { foreach ($Lookup[$lup]['Subtable'][$lus]['subs'] as $luss) { $lookupGlyphs = $luss['Replace']; $mLen = count($lookupGlyphs); @@ -2628,7 +2641,7 @@ function _getGSUBarray(&$Lookup, &$lul, $scripttag) $lup = $Lookup[$i]['Subtable'][$c]['SubstLookupRecord'][$b]['LookupListIndex']; $seqIndex = $Lookup[$i]['Subtable'][$c]['SubstLookupRecord'][$b]['SequenceIndex']; for ($lus = 0; $lus < $Lookup[$lup]['SubtableCount']; $lus++) { - if (count($Lookup[$lup]['Subtable'][$lus]['subs'])) { + if (!empty($Lookup[$lup]['Subtable'][$lus]['subs'])) { foreach ($Lookup[$lup]['Subtable'][$lus]['subs'] as $luss) { $lookupGlyphs = $luss['Replace']; $mLen = count($lookupGlyphs); @@ -2707,7 +2720,7 @@ function _getGSUBarray(&$Lookup, &$lul, $scripttag) // $Lookup[$lup] = secondary Lookup for ($lus = 0; $lus < $Lookup[$lup]['SubtableCount']; $lus++) { - if (count($Lookup[$lup]['Subtable'][$lus]['subs'])) { + if (!empty($Lookup[$lup]['Subtable'][$lus]['subs'])) { foreach ($Lookup[$lup]['Subtable'][$lus]['subs'] as $luss) { $lookupGlyphs = $luss['Replace']; $mLen = count($lookupGlyphs); @@ -2823,7 +2836,7 @@ function _getGSUBarray(&$Lookup, &$lul, $scripttag) // $Lookup[$lup] = secondary Lookup for ($lus = 0; $lus < $Lookup[$lup]['SubtableCount']; $lus++) { - if (count($Lookup[$lup]['Subtable'][$lus]['subs'])) { + if (!empty($Lookup[$lup]['Subtable'][$lus]['subs'])) { foreach ($Lookup[$lup]['Subtable'][$lus]['subs'] as $luss) { $lookupGlyphs = $luss['Replace']; $mLen = count($lookupGlyphs); @@ -2946,14 +2959,43 @@ function _checkGSUBignore($flag, $glyph, $MarkFilteringSet) $ignore = true; } } - // Flag & 0x0010 = UseMarkFilteringSet - if (($flag & 0x0010) && strpos($this->MarkGlyphSets[$MarkFilteringSet], $glyph)) { + // Flag & 0x0010 = UseMarkFilteringSet: skip every mark *except* those in the set + if (($flag & 0x0010) && strpos($this->GlyphClassMarks, $glyph) + && !strpos($this->MarkGlyphSets[$MarkFilteringSet], $glyph)) { $ignore = true; } return $ignore; } + + /** + * UseMarkFilteringSet means "skip every mark except those in the given mark glyph set", so the glyphs to + * ignore are GlyphClassMarks minus that set - not the set itself. + * + * @param string $marks Space-prefixed, "|"-separated glyph list, e.g. " 00DCA| 00DD2" + * @param string $set The mark glyph set, in the same format + * + * @return string + */ + private function marksOutsideFilteringSet($marks, $set) + { + $keep = []; + $inSet = []; + foreach (explode('|', $set) as $glyph) { + $inSet[trim($glyph)] = true; + } + + foreach (explode('|', $marks) as $glyph) { + $glyph = trim($glyph); + if ($glyph !== '' && !isset($inSet[$glyph])) { + $keep[] = $glyph; + } + } + + return $keep ? ' ' . implode('| ', $keep) : ''; + } + function _getGSUBignoreString($flag, $MarkFilteringSet) { // If ignoreFlag set, combine all ignore glyphs into -> "((?:(?: FBA1| FBA2| FBA3))*)" @@ -2976,7 +3018,8 @@ function _getGSUBignoreString($flag, $MarkFilteringSet) if (!isset($this->MarkGlyphSets[$MarkFilteringSet])) { throw new \Mpdf\Exception\FontException(sprintf('Font "%s" uses mark filtering set %s, which GDEF does not define', $this->fontkey, $MarkFilteringSet)); } - $str = $this->MarkGlyphSets[$MarkFilteringSet]; + $ignoreflag = $flag; + $str = $this->marksOutsideFilteringSet($this->GlyphClassMarks, $this->MarkGlyphSets[$MarkFilteringSet]); } // If Ignore Marks set, supercedes any above diff --git a/tests/Mpdf/TTFontFileTest.php b/tests/Mpdf/TTFontFileTest.php index 66a90d509..9e4320240 100644 --- a/tests/Mpdf/TTFontFileTest.php +++ b/tests/Mpdf/TTFontFileTest.php @@ -47,4 +47,17 @@ public function testGetMetricsWithMarkGlyphSets() $this->assertSame('NotoSansSinhala-Regular', $this->ttf->fullName); } + /** + * MarkGlyphSetsDef coverage offsets are relative to that table, not to the file. Seeking to them as + * absolute offsets lands in the table directory and yields empty sets, which silently disables every + * UseMarkFilteringSet lookup. U+0DCA/U+0DD2/U+0DD3 are the subset's Sinhala marks; the second set's + * glyph has no cmap entry, so it is mapped into the Private Use Area. + */ + public function testGetMetricsReadsMarkGlyphSetsCoverage() + { + $this->ttf->getMetrics(__DIR__ . '/../data/ttf/NotoSansSinhala-Subset.ttf', (string) time(), 0, false, false, 0xFF); + + $this->assertSame([' 00DCA| 00DD2| 00DD3', ' 0E00A'], $this->ttf->MarkGlyphSets); + } + } diff --git a/tests/Snapshots/MarkGlyphSetsSnapshotTest.php b/tests/Snapshots/MarkGlyphSetsSnapshotTest.php new file mode 100644 index 000000000..1df905c6e --- /dev/null +++ b/tests/Snapshots/MarkGlyphSetsSnapshotTest.php @@ -0,0 +1,51 @@ +mpdf and + * loading it with content + * + * @return void + * @internal Don't call any $this->mpdf->Output*() method + */ + public function generatePdf() + { + $this->mpdf = new \Mpdf\Mpdf([ + 'fontDir' => [__DIR__ . '/../data/ttf'], + 'fontdata' => [ + 'sinhalasubset' => [ + 'R' => 'NotoSansSinhala-Subset.ttf', + 'useOTL' => 0xFF, + ], + ], + 'default_font' => 'sinhalasubset', + 'default_font_size' => 30, + ]); + + $this->mpdf->WriteHTML( + '