diff --git a/src/Fonts/GlyphString.php b/src/Fonts/GlyphString.php index ef7847fc5..10308767d 100644 --- a/src/Fonts/GlyphString.php +++ b/src/Fonts/GlyphString.php @@ -5,13 +5,14 @@ /** * The fixed-width hexadecimal strings the OTL code writes characters as. * - * Coverage tables, mark and ligature classes and substitution rules are all carried as text and - * compared as text: GlyphClassMarks is one run of them that membership is tested against with - * strpos(), and a substitution of one character by several is their concatenation. Both only work - * while every character takes up the same number of characters, which is what this is for. + * Coverage tables, mark and ligature classes and substitution rules are all carried as text, and a + * substitution of one character by several is their concatenation, which only works while every + * character takes up the same number of characters. That is what this is for. * * Five digits is that width. It covers everything up to U+FFFFF; a character above that - plane 16, - * the second private use area - comes back six digits wide and lines up with nothing. + * the second private use area - comes back six digits wide and lines up with nothing. A six-digit + * glyph holds two five-digit ones, U+100300 both U+10030 and U+0300, so a list of glyphs is never + * searched for one with strpos(): set() and inList() test it glyph by glyph. */ class GlyphString { @@ -26,4 +27,64 @@ public static function of($codepoint) return str_pad(strtoupper(dechex($codepoint)), 5, '0', STR_PAD_LEFT); } + /** + * A list of glyphs as a set, for a list asked about glyph after glyph. + * + * @param string $glyphs "|"-separated, with or without the space GDEF's classes put before each + * glyph: " 00300| 00301" + * + * @return true[] Glyph, as hex => true + */ + public static function set($glyphs) + { + $set = []; + foreach (explode('|', $glyphs) as $glyph) { + $glyph = trim($glyph); + if ($glyph !== '') { + $set[$glyph] = true; + } + } + + return $set; + } + + /** + * Whether a list of glyphs names this one, for a list asked about too seldom to be worth a set. + * + * The glyph has to stand on its own: the list may separate glyphs with anything but a hex digit, + * so the one test serves GDEF's " 00300| 00301", a Coverage table's "00300|00301", the space-ended + * "0FE8E 0FE94 " of `finals`, and an ignore pattern's "((?:(?: 00300| 00301))*)". + * + * @param string $glyphs + * @param string $glyph As hex + * + * @return bool + */ + public static function inList($glyphs, $glyph) + { + $length = strlen($glyph); + + for ($at = strpos($glyphs, $glyph); $at !== false; $at = strpos($glyphs, $glyph, $at + 1)) { + if (!self::isHexDigitAt($glyphs, $at - 1) && !self::isHexDigitAt($glyphs, $at + $length)) { + return true; + } + } + + return false; + } + + /** + * Whether a match in a list is flanked by another hex digit, which makes it part of a longer glyph + * rather than a glyph of its own. + * + * @param string $string The list being searched + * @param int $at A position either side of a match, which may fall outside the string + * + * @return bool False before the start or past the end, where nothing flanks the match + */ + private static function isHexDigitAt($string, $at) + { + return $at >= 0 && isset($string[$at]) && strpos('0123456789ABCDEFabcdef', $string[$at]) !== false; + } + } diff --git a/src/Fonts/Table/LookupFlag.php b/src/Fonts/Table/LookupFlag.php index 9b93d60fc..3c59eeea7 100644 --- a/src/Fonts/Table/LookupFlag.php +++ b/src/Fonts/Table/LookupFlag.php @@ -3,6 +3,7 @@ namespace Mpdf\Fonts\Table; use Mpdf\Exception\FontException; +use Mpdf\Fonts\GlyphString; /** * The glyphs a lookup passes over, from its LookupFlag, its markFilteringSet and GDEF. @@ -48,6 +49,11 @@ class LookupFlag */ private $marksOutsideFilteringSets = []; + /** + * @var true[][] Each class skips() has been asked about, as GlyphString::set() gives it + */ + private $sets = []; + /** * @param string $fontkey * @param array $gdef @@ -132,7 +138,8 @@ public function glyphs($flag, $markFilteringSet) * * The same answer as looking for the glyph in glyphs(), a class at a time, without joining the * classes: the shaper asks once per glyph a subtable is offered, and a font's marks can run to tens - * of kilobytes of text. + * of kilobytes of text. Each class is looked up in a set rather than searched, so a glyph is not + * found inside a longer one's hex. * * @param int $flag The lookup's LookupFlag * @param string $glyph The glyph, as hex @@ -145,7 +152,8 @@ public function skips($flag, $glyph, $markFilteringSet) $this->checkMarkFilteringSet($flag, $markFilteringSet); foreach (self::skipped($flag) as $class) { - if (strpos($this->glyphsOf($class, $flag, $markFilteringSet), $glyph)) { + $set = $this->setOf($class, $flag, $markFilteringSet); + if (isset($set[$glyph])) { return true; } } @@ -183,6 +191,43 @@ private function glyphsOf($class, $flag, $markFilteringSet) } } + /** + * @return true[] GDEF's marks, as GlyphString::set() gives them + */ + public function marks() + { + return $this->setOf(self::MARKS, 0, ''); + } + + /** + * A class skips() tests glyphs against, built once and kept, since it is asked glyph after glyph. + * + * The marks outside a filtering set or an attachment class depend on which set or class the flag + * names, so those are kept per set or class; every other class is the same for any flag. + * + * @param string $class One of the class constants, as skipped() gives it + * @param int $flag The lookup's LookupFlag, which names the attachment class + * @param int|string $markFilteringSet The mark glyph set it names, or '' where it names none + * + * @return true[] The class, as GlyphString::set() gives it + */ + private function setOf($class, $flag, $markFilteringSet) + { + if ($class === self::MARKS_OUTSIDE_FILTERING_SET) { + $key = $class . $markFilteringSet; + } elseif ($class === self::MARKS_OUTSIDE_ATTACHMENT_CLASS) { + $key = $class . self::attachmentClass($flag); + } else { + $key = $class; + } + + if (!isset($this->sets[$key])) { + $this->sets[$key] = GlyphString::set($this->glyphsOf($class, $flag, $markFilteringSet)); + } + + return $this->sets[$key]; + } + /** * UseMarkFilteringSet means "skip every mark except those in the given mark glyph set", so the * glyphs to skip are the marks minus that set - not the set itself. @@ -196,10 +241,7 @@ private function marksOutsideFilteringSet($markFilteringSet) } $keep = []; - $inSet = []; - foreach (explode('|', $this->gdef['MarkGlyphSets'][$markFilteringSet]) as $glyph) { - $inSet[trim($glyph)] = true; - } + $inSet = GlyphString::set($this->gdef['MarkGlyphSets'][$markFilteringSet]); foreach (explode('|', $this->gdef['GlyphClassMarks']) as $glyph) { $glyph = trim($glyph); diff --git a/src/Otl.php b/src/Otl.php index b93ca3255..4f4675829 100644 --- a/src/Otl.php +++ b/src/Otl.php @@ -102,6 +102,21 @@ class Otl */ private $lookupFlag; + /** + * GlyphClassMarks for the current font, as GlyphString::set() gives it + * + * @var true[] + */ + private $marks; + + /** + * $lookupFlag for every font laid out so far, by font key: the sets it builds from GDEF are kept + * with it, and a document sets one font for run after run + * + * @var LookupFlag[] + */ + private $lookupFlags = []; + var $Ignores; var $LuCoverage; @@ -290,7 +305,23 @@ private function loadGdefData() $this->GlyphClassLigatures = $gdef['GlyphClassLigatures']; $this->GlyphClassComponents = $gdef['GlyphClassComponents']; $this->GlyphClassBases = $gdef['GlyphClassBases']; - $this->lookupFlag = new LookupFlag($this->fontkey, $gdef); + + if (!isset($this->lookupFlags[$this->fontkey])) { + $this->lookupFlags[$this->fontkey] = new LookupFlag($this->fontkey, $gdef); + } + + $this->lookupFlag = $this->lookupFlags[$this->fontkey]; + $this->marks = $this->lookupFlag->marks(); + } + + /** + * @param string $hex A glyph, as GlyphString::of() writes it + * + * @return bool Whether GDEF classes it as a mark + */ + private function isMark($hex) + { + return isset($this->marks[$hex]); } /** @@ -349,7 +380,7 @@ private function analyseCharacters($str) $charasstr = GlyphString::of($char); - if (strpos($this->GlyphClassMarks, $charasstr) !== false) { + if ($this->isMark($charasstr)) { $OTLdata[$subchunk][$charctr]['group'] = 'M'; } elseif ($char == 32 || $char == 12288) { $OTLdata[$subchunk][$charctr]['group'] = 'S'; @@ -404,7 +435,7 @@ private function markWordBoundaries($scriptblock) if ($this->usesWordBoundaryDictionary()) { $dict = $this->lineBreakDictionary(); if ($dict !== null) { - LineBreaking::southEastAsian($this->OTLdata, $dict, $this->GlyphClassMarks); + LineBreaking::southEastAsian($this->OTLdata, $dict, $this->marks); } } elseif ($this->usesTibetanWordBoundaries($scriptblock)) { LineBreaking::tibetan($this->OTLdata); @@ -613,7 +644,7 @@ private function shapeArabic($GSUBscriptTag, $GSUBlangsys, $GSUBFeatures, $scrip // Position: After the character elseif ($this->OTLdata[$i]['uni'] == 0xFEB3 || $this->OTLdata[$i]['uni'] == 0xFEB4 || $this->OTLdata[$i]['uni'] == 0xFEBB || $this->OTLdata[$i]['uni'] == 0xFEBC) { $checkpos = $i + 1; - while (isset($this->OTLdata[$checkpos]) && strpos($this->GlyphClassMarks, $this->OTLdata[$checkpos]['hex']) !== false) { + while (isset($this->OTLdata[$checkpos]) && $this->isMark($this->OTLdata[$checkpos]['hex'])) { $checkpos++; } if (isset($this->OTLdata[$checkpos])) { @@ -639,7 +670,7 @@ private function shapeArabic($GSUBscriptTag, $GSUBlangsys, $GSUBFeatures, $scrip elseif ($this->OTLdata[$i]['uni'] == 0xFEAE || $this->OTLdata[$i]['uni'] == 0xFEF2 || $this->OTLdata[$i]['uni'] == 0xFEF0 || $this->OTLdata[$i]['uni'] == 0xFEF4 || $this->OTLdata[$i]['uni'] == 0xFBE9 || $this->OTLdata[$i]['uni'] == 0xFBFD || $this->OTLdata[$i]['uni'] == 0xFBFF ) { $checkpos = $i - 1; - while (isset($this->OTLdata[$checkpos]) && strpos($this->GlyphClassMarks, $this->OTLdata[$checkpos]['hex']) !== false) { + while (isset($this->OTLdata[$checkpos]) && $this->isMark($this->OTLdata[$checkpos]['hex'])) { $checkpos--; } if (isset($this->OTLdata[$checkpos]) && $this->OTLdata[$checkpos]['uni'] == 0xFE92) { @@ -664,7 +695,7 @@ private function shapeArabic($GSUBscriptTag, $GSUBlangsys, $GSUBFeatures, $scrip */ if (!isset($this->OTLdata[$i]['GPOSinfo']['kashida'])) { - if (strpos($this->GSUBdata[$this->GSUBfont]['finals'], $this->OTLdata[$i]['hex']) !== false) { // ANY OTHER FINAL FORM + if (GlyphString::inList($this->GSUBdata[$this->GSUBfont]['finals'], $this->OTLdata[$i]['hex'])) { // ANY OTHER FINAL FORM $this->OTLdata[$i]['GPOSinfo']['kashida'] = 2; } elseif (strpos('0FEAE 0FEF0 0FEF2', $this->OTLdata[$i]['hex']) !== false) { // not already included in 5 above $this->OTLdata[$i]['GPOSinfo']['kashida'] = 1; @@ -720,7 +751,7 @@ private function shapeIndic($GSUBscriptTag, $GSUBlangsys, $GSUBFeatures, $script $newinfo[$i]['general_category'] = $ucd_record[0]; $newinfo[$i]['bidi_type'] = $ucd_record[2]; $charasstr = GlyphString::of($sub[$i]); - if (strpos($this->GlyphClassMarks, $charasstr) !== false) { + if ($this->isMark($charasstr)) { $newinfo[$i]['group'] = 'M'; } else { $newinfo[$i]['group'] = 'C'; @@ -1018,7 +1049,7 @@ private function shapeGeneric($GSUBscriptTag, $GSUBlangsys, $GSUBFeatures, $scri $newinfo[0]['general_category'] = $ucd_record[0]; $newinfo[0]['bidi_type'] = $ucd_record[2]; $charasstr = GlyphString::of($sub[0]); - if (strpos($this->GlyphClassMarks, $charasstr) !== false) { + if ($this->isMark($charasstr)) { $newinfo[0]['group'] = 'M'; } else { $newinfo[0]['group'] = 'C'; @@ -1045,7 +1076,7 @@ private function shapeGeneric($GSUBscriptTag, $GSUBlangsys, $GSUBFeatures, $scri $newinfo[0]['general_category'] = $ucd_record[0]; $newinfo[0]['bidi_type'] = $ucd_record[2]; $charasstr = GlyphString::of($sub[1]); - if (strpos($this->GlyphClassMarks, $charasstr) !== false) { + if ($this->isMark($charasstr)) { $newinfo[0]['group'] = 'M'; } else { $newinfo[0]['group'] = 'C'; @@ -1198,7 +1229,7 @@ private function applyGPOS($GPOSscriptTag, $GPOSlangsys, $GPOSFeatures, $scriptb for ($i = (count($this->OTLdata) - 1); $i >= 0; $i--) { if (isset($this->Entry[$i]) && isset($this->Entry[$i]['Y']) && $this->Entry[$i]['dir'] == 'RTL') { $nextbase = $i - 1; // Set as next base ignoring marks (next base reading RTL in logical oder - while (isset($this->OTLdata[$nextbase]['hex']) && strpos($this->GlyphClassMarks, $this->OTLdata[$nextbase]['hex']) !== false) { + while (isset($this->OTLdata[$nextbase]['hex']) && $this->isMark($this->OTLdata[$nextbase]['hex'])) { $nextbase--; } if (isset($this->Exit[$nextbase]) && isset($this->Exit[$nextbase]['Y'])) { @@ -1228,7 +1259,7 @@ private function applyGPOS($GPOSscriptTag, $GPOSlangsys, $GPOSFeatures, $scriptb } else { $incurs = false; } - } elseif (strpos($this->GlyphClassMarks, $this->OTLdata[$i]['hex']) !== false) { + } elseif ($this->isMark($this->OTLdata[$i]['hex'])) { continue; } // ignore Marks else { @@ -1240,7 +1271,7 @@ private function applyGPOS($GPOSscriptTag, $GPOSlangsys, $GPOSFeatures, $scriptb for ($i = 0; $i < count($this->OTLdata); $i++) { if (isset($this->Exit[$i]) && isset($this->Exit[$i]['Y']) && $this->Exit[$i]['dir'] == 'LTR') { $nextbase = $i + 1; // Set as next base ignoring marks - while (isset($this->OTLdata[$nextbase]['hex']) && strpos($this->GlyphClassMarks, $this->OTLdata[$nextbase]['hex']) !== false) { + while (isset($this->OTLdata[$nextbase]['hex']) && $this->isMark($this->OTLdata[$nextbase]['hex'])) { $nextbase++; } if (isset($this->Entry[$nextbase]) && isset($this->Entry[$nextbase]['Y'])) { @@ -1270,7 +1301,7 @@ private function applyGPOS($GPOSscriptTag, $GPOSlangsys, $GPOSFeatures, $scriptb } else { $incurs = false; } - } elseif (strpos($this->GlyphClassMarks, $this->OTLdata[$i]['hex']) !== false) { + } elseif ($this->isMark($this->OTLdata[$i]['hex'])) { continue; } // ignore Marks else { @@ -2736,7 +2767,7 @@ function GSUBsubstitute($pos, $substitute, $Type, $GlyphPos = null) $bt = $this->OTLdata[$pos]['bidi_type']; // } - if (strpos($this->GlyphClassMarks, $newOTLdata[$i]['hex']) !== false) { + if ($this->isMark($newOTLdata[$i]['hex'])) { $gp = 'M'; } elseif ($uni == 32) { $gp = 'S'; @@ -2806,7 +2837,7 @@ function GSUBsubstitute($pos, $substitute, $Type, $GlyphPos = null) if ($this->restrictToSyllable && isset($this->OTLdata[$GlyphPos[$i]]['syllable']) && $this->OTLdata[$GlyphPos[$i]]['syllable'] != $current_syllable) { return 0; } - if (strpos($this->GlyphClassMarks, $unistr) !== false) { + if ($this->isMark($unistr)) { $contains_marks = true; } else { $contains_nonmarks = true; @@ -2900,7 +2931,7 @@ function GSUBsubstitute($pos, $substitute, $Type, $GlyphPos = null) // While next char to right is a mark (but not the next matched glyph) // ?? + also include a Mark Ligature here $ic = 1; - while ((($i == count($GlyphPos) - 1) || (isset($GlyphPos[$i + 1]) && ($GlyphPos[$i] + $ic) < $GlyphPos[$i + 1])) && isset($this->OTLdata[($GlyphPos[$i] + $ic)]) && strpos($this->GlyphClassMarks, $this->OTLdata[($GlyphPos[$i] + $ic)]['hex']) !== false) { + while ((($i == count($GlyphPos) - 1) || (isset($GlyphPos[$i + 1]) && ($GlyphPos[$i] + $ic) < $GlyphPos[$i + 1])) && isset($this->OTLdata[($GlyphPos[$i] + $ic)]) && $this->isMark($this->OTLdata[($GlyphPos[$i] + $ic)]['hex'])) { $newComp = $currComp; if (isset($this->assocMarks[$GlyphPos[$i] + $ic])) { // One of the inbetween Marks is already associated with a Lig // OK as long as it is associated with the current Lig @@ -2925,7 +2956,7 @@ function GSUBsubstitute($pos, $substitute, $Type, $GlyphPos = null) $bt = $this->OTLdata[$pos]['bidi_type']; // } - if (strpos($this->GlyphClassMarks, GlyphString::of($substitute)) !== false) { + if ($this->isMark(GlyphString::of($substitute))) { $gp = 'M'; } elseif ($substitute == 32) { $gp = 'S'; @@ -3126,7 +3157,7 @@ private function _applyGPOSvaluerecord($basepos, $Value) // If current glyph is a mark with a defined width, any XAdvance is considered to REPLACE the character Advance Width // Test case
င်္က္ကျြွေိ
- if (strpos($this->GlyphClassMarks, $this->OTLdata[$basepos]['hex']) !== false) { + if ($this->isMark($this->OTLdata[$basepos]['hex'])) { $cw = round($this->mpdf->_getCharWidth($this->mpdf->CurrentFont['cw'], $this->OTLdata[$basepos]['uni']) * $this->mpdf->CurrentFont['unitsPerEm'] / 1000); // convert back to font design units } else { $cw = 0; @@ -3189,11 +3220,11 @@ private function _getXAdvancePos($pos) { // NB Not all fonts have all marks specified in GlyphClassMarks // If the current glyph is not a base (but a mark) then ignore this, and apply to the current position - if (strpos($this->GlyphClassMarks, $this->OTLdata[$pos]['hex']) !== false) { + if ($this->isMark($this->OTLdata[$pos]['hex'])) { return $pos; } - while (isset($this->OTLdata[$pos + 1]['hex']) && strpos($this->GlyphClassMarks, $this->OTLdata[$pos + 1]['hex']) !== false) { + while (isset($this->OTLdata[$pos + 1]['hex']) && $this->isMark($this->OTLdata[$pos + 1]['hex'])) { $pos++; } return $pos; @@ -3551,7 +3582,7 @@ private function _applyGPOSmarkToBase($lookupID, $subtable, $ptr, $currGlyph, $c $BaseArray = $subtable_offset + $this->reader->readUInt16(); // Offset to BaseArray table $this->reader->seek($BaseCoverage); - $BaseGlyphs = implode('|', $this->_getCoverage()); + $BaseGlyphs = $this->coverageIndexByHex(); $checkpos = $ptr; $checkpos--; @@ -3563,16 +3594,16 @@ private function _applyGPOSmarkToBase($lookupID, $subtable, $ptr, $currGlyph, $c // This Fix blocks the GPOS rule if the "mark" is not actually classified as a mark in the GlyphClasses of GDEF // but only in Indic old-spec. // Test cases: ನ್ನು and ಕ್ರೌ - if ($this->shaper == 'I' && $is_old_spec && strpos($this->GlyphClassMarks, $this->OTLdata[$ptr]['hex']) === false) { + if ($this->shaper == 'I' && $is_old_spec && !$this->isMark($this->OTLdata[$ptr]['hex'])) { return; } // "To identify the base glyph that combines with a mark, the text-processing client must look backward in the glyph string from the mark to the preceding base glyph." - while (isset($this->OTLdata[$checkpos]) && strpos($this->GlyphClassMarks, $this->OTLdata[$checkpos]['hex']) !== false) { + while (isset($this->OTLdata[$checkpos]) && $this->isMark($this->OTLdata[$checkpos]['hex'])) { $checkpos--; } - if (isset($this->OTLdata[$checkpos]) && strpos($BaseGlyphs, $this->OTLdata[$checkpos]['hex']) !== false) { + if (isset($this->OTLdata[$checkpos]) && isset($BaseGlyphs[$this->OTLdata[$checkpos]['hex']])) { $matchedpos = $checkpos; } else { $matchedpos = false; @@ -3586,7 +3617,7 @@ private function _applyGPOSmarkToBase($lookupID, $subtable, $ptr, $currGlyph, $c // Get the relevant BaseRecord $this->reader->seek($BaseArray); $BaseCount = $this->reader->readUInt16(); - $BasePos = strpos($BaseGlyphs, $this->OTLdata[$matchedpos]['hex']) / 6; + $BasePos = $BaseGlyphs[$this->OTLdata[$matchedpos]['hex']]; // Move to the BaseRecord we want $nSkip = (2 * $BasePos * $ClassCount ); @@ -3643,17 +3674,17 @@ private function _applyGPOSmarkToLigature($lookupID, $subtable, $ptr, $currGlyph $LigatureArray = $subtable_offset + $this->reader->readUInt16(); // Offset to LigatureArray table $this->reader->seek($LigatureCoverage); - $LigatureGlyphs = implode('|', $this->_getCoverage()); + $LigatureGlyphs = $this->coverageIndexByHex(); $checkpos = $ptr; $checkpos--; // "To position a combining mark using a MarkToLigature attachment subtable, the text-processing client must work backward from the mark to the preceding ligature glyph." - while (isset($this->OTLdata[$checkpos]) && strpos($this->GlyphClassMarks, $this->OTLdata[$checkpos]['hex']) !== false) { + while (isset($this->OTLdata[$checkpos]) && $this->isMark($this->OTLdata[$checkpos]['hex'])) { $checkpos--; } - if (isset($this->OTLdata[$checkpos]) && strpos($LigatureGlyphs, $this->OTLdata[$checkpos]['hex']) !== false) { + if (isset($this->OTLdata[$checkpos]) && isset($LigatureGlyphs[$this->OTLdata[$checkpos]['hex']])) { $matchedpos = $checkpos; } else { $matchedpos = false; @@ -3667,7 +3698,7 @@ private function _applyGPOSmarkToLigature($lookupID, $subtable, $ptr, $currGlyph // Get the relevant LigatureRecord $this->reader->seek($LigatureArray); $LigatureCount = $this->reader->readUInt16(); - $LigaturePos = strpos($LigatureGlyphs, $this->OTLdata[$matchedpos]['hex']) / 6; + $LigaturePos = $LigatureGlyphs[$this->OTLdata[$matchedpos]['hex']]; // Move to the LigatureAttach table Record we want $nSkip = (2 * $LigaturePos); @@ -3748,13 +3779,13 @@ private function _applyGPOSmarkToMark($lookupID, $subtable, $ptr, $currGlyph, $c $Mark1Array = $subtable_offset + $this->reader->readUInt16(); // Offset to MarkArray table $Mark2Array = $subtable_offset + $this->reader->readUInt16(); // Offset to Mark2Array table $this->reader->seek($Mark2Coverage); - $Mark2Glyphs = implode('|', $this->_getCoverage()); + $Mark2Glyphs = $this->coverageIndexByHex(); $checkpos = $ptr; $checkpos--; while (isset($this->OTLdata[$checkpos]) && isset($ignore[$this->OTLdata[$checkpos]['uni']])) { $checkpos--; } - if (isset($this->OTLdata[$checkpos]) && strpos($Mark2Glyphs, $this->OTLdata[$checkpos]['hex']) !== false) { + if (isset($this->OTLdata[$checkpos]) && isset($Mark2Glyphs[$this->OTLdata[$checkpos]['hex']])) { $matchedpos = $checkpos; } else { $matchedpos = false; @@ -3768,7 +3799,7 @@ private function _applyGPOSmarkToMark($lookupID, $subtable, $ptr, $currGlyph, $c // Get the relevant Mark2Record $this->reader->seek($Mark2Array); $Mark2Count = $this->reader->readUInt16(); - $Mark2Pos = strpos($Mark2Glyphs, $this->OTLdata[$matchedpos]['hex']) / 6; + $Mark2Pos = $Mark2Glyphs[$this->OTLdata[$matchedpos]['hex']]; // Move to the Mark2Record we want $nSkip = (2 * $Mark2Pos * $ClassCount ); @@ -4578,7 +4609,7 @@ private function glyphToChar($gid) * The glyph IDs a Coverage table covers, for a Single Substitution Format 1, which adds a delta * to a glyph ID rather than naming a replacement. * - * Cached apart from _getCoverage below: the same table, projected differently. + * Cached apart from coverageIndexByHex below: the same table, projected differently. */ private function _getCoverageGID() { @@ -4592,22 +4623,28 @@ private function _getCoverageGID() } /** - * The characters a Coverage table covers, as the hex strings the shaper matches against + * The characters a Coverage table covers, each with its Coverage Index, for the mark attachment + * subtables, which find the glyph a mark attaches to and then index a parallel array by it. + * + * @return int[] hex => Coverage Index, the first where two glyphs stand for one character */ - private function _getCoverage() + private function coverageIndexByHex() { $offset = $this->reader->tell(); - if (!isset($this->LuDataCache[$this->otlCacheKey]['coverage'][$offset])) { - $g = []; - foreach (Coverage::glyphs($this->reader) as $glyphID) { - $g[] = GlyphString::of($this->glyphToChar($glyphID)); + if (!isset($this->LuDataCache[$this->otlCacheKey]['coverageIndex'][$offset])) { + $indexes = []; + foreach (Coverage::glyphs($this->reader) as $index => $glyphID) { + $hex = GlyphString::of($this->glyphToChar($glyphID)); + if (!isset($indexes[$hex])) { + $indexes[$hex] = $index; + } } - $this->LuDataCache[$this->otlCacheKey]['coverage'][$offset] = $g; + $this->LuDataCache[$this->otlCacheKey]['coverageIndex'][$offset] = $indexes; } - return $this->LuDataCache[$this->otlCacheKey]['coverage'][$offset]; + return $this->LuDataCache[$this->otlCacheKey]['coverageIndex'][$offset]; } /** @@ -4620,7 +4657,7 @@ private function _getCoverage() * thousands and whose Coverage tables name thousands of glyphs, that scanning was most of the time * spent shaping a word. * - * Cached apart from _getCoverage above: the same table, projected differently. + * Cached apart from coverageIndexByHex above: the same table, projected differently. * * @return array map of unicode => 1 */ diff --git a/src/OtlDump.php b/src/OtlDump.php index 20159d1e7..02a953b4d 100644 --- a/src/OtlDump.php +++ b/src/OtlDump.php @@ -1624,10 +1624,10 @@ private function contextExample(array $exampleB, array $exampleI, array $example private function positionHolds($coverage, $class0excl, $glyph) { if ($coverage === '' && $class0excl !== '') { - return strpos($class0excl, $glyph) === false; + return !GlyphString::inList($class0excl, $glyph); } - return strpos($coverage, $glyph) !== false; + return GlyphString::inList($coverage, $glyph); } /** diff --git a/src/Shaper/Arabic.php b/src/Shaper/Arabic.php index b65ec496e..fca5d4bbd 100644 --- a/src/Shaper/Arabic.php +++ b/src/Shaper/Arabic.php @@ -2,6 +2,7 @@ namespace Mpdf\Shaper; +use Mpdf\Fonts\GlyphString; use Mpdf\Utils\UtfString; /** @@ -301,18 +302,21 @@ private static function glyphs($char, $type, &$chars, $i, $scriptTag, $usetags, if ($retk != -1) { $match = true; // If GSUB includes a Backtrack or Lookahead condition (e.g. font ArabicTypesetting) + // The walk over ignored glyphs stops at the edge of the run, and a position it runs out before + // reaching is not held, as in HarfBuzz's match_backtrack() and match_lookahead(). Past the edge + // the glyph is null, which inList() finds in any pattern from PHP 8, so the walk would not end. if (isset($arabGlyphs[$char]['prel'][$retk]) && $arabGlyphs[$char]['prel'][$retk]) { $ig = 1; foreach ($arabGlyphs[$char]['prel'][$retk] as $k => $v) { // $k starts 0, 1... if (!isset($chars[$i - $ig - $k])) { $match = false; - } elseif (strpos($v, $chars[$i - $ig - $k]) === false) { - while (strpos($arabGlyphs[$char]['ignore'][$retk], $chars[$i - $ig - $k]) !== false) { // ignore + } elseif (!GlyphString::inList($v, $chars[$i - $ig - $k])) { + while (isset($chars[$i - $ig - $k]) && GlyphString::inList($arabGlyphs[$char]['ignore'][$retk], $chars[$i - $ig - $k])) { $ig++; } if (!isset($chars[$i - $ig - $k])) { $match = false; - } elseif (strpos($v, $chars[$i - $ig - $k]) === false) { + } elseif (!GlyphString::inList($v, $chars[$i - $ig - $k])) { $match = false; } } @@ -323,13 +327,13 @@ private static function glyphs($char, $type, &$chars, $i, $scriptTag, $usetags, foreach ($arabGlyphs[$char]['postl'][$retk] as $k => $v) { // $k starts 0, 1... if (!isset($chars[$i + $ig + $k])) { $match = false; - } elseif (strpos($v, $chars[$i + $ig + $k]) === false) { - while (strpos($arabGlyphs[$char]['ignore'][$retk], $chars[$i + $ig + $k]) !== false) { // ignore + } elseif (!GlyphString::inList($v, $chars[$i + $ig + $k])) { + while (isset($chars[$i + $ig + $k]) && GlyphString::inList($arabGlyphs[$char]['ignore'][$retk], $chars[$i + $ig + $k])) { $ig++; } if (!isset($chars[$i + $ig + $k])) { $match = false; - } elseif (strpos($v, $chars[$i + $ig + $k]) === false) { + } elseif (!GlyphString::inList($v, $chars[$i + $ig + $k])) { $match = false; } } diff --git a/src/Shaper/LineBreaking.php b/src/Shaper/LineBreaking.php index 260b6e5e2..02139a901 100644 --- a/src/Shaper/LineBreaking.php +++ b/src/Shaper/LineBreaking.php @@ -46,9 +46,10 @@ public static function tibetan(&$info) * ends and the next begins. That cannot be read off the characters - it needs a dictionary, walked * as a trie over the low byte of each codepoint. * - * @param string $dict The dictionary as loaded from the font package, in the format wordMatch() walks + * @param string $dict The dictionary as loaded from the font package, in the format wordMatch() walks + * @param true[] $marks GDEF's marks, as GlyphString::set() gives them: a word does not end before one */ - public static function southEastAsian(&$info, $dict, $glyphClassMarks) + public static function southEastAsian(&$info, $dict, $marks) { // Find all word boundaries and mark end of word $info[$i]['wordend']=true on last character // If Thai, allow for possible suffixes (not in Lao or Khmer) @@ -65,7 +66,7 @@ public static function southEastAsian(&$info, $dict, $glyphClassMarks) $matches = $rollover; $rollover = []; } else { - $matches = self::wordMatch($dict, $info, $glyphClassMarks, $ptr); + $matches = self::wordMatch($dict, $info, $marks, $ptr); } if (count($matches) == 1) { $matchpos = $matches[0]; @@ -87,7 +88,7 @@ public static function southEastAsian(&$info, $dict, $glyphClassMarks) for ($m = count($matches) - 1; $m >= 0; $m--) { //for ($m=0;$m'; // Do not match if next character in text is a Mark - if (isset($info[$ptr]['uni']) && strpos($glyphClassMarks, $info[$ptr]['hex']) === false) { + if (isset($info[$ptr]['uni']) && !isset($marks[$info[$ptr]['hex']])) { $matches[] = $ptr - 1; } $dictptr++; } elseif ($x == self::FINAL_MATCH) { //echo "DICT_FINAL_MATCH: ".dechex($c).'
'; // Do not match if next character in text is a Mark - if (isset($info[$ptr]['uni']) && strpos($glyphClassMarks, $info[$ptr]['hex']) === false) { + if (isset($info[$ptr]['uni']) && !isset($marks[$info[$ptr]['hex']])) { $matches[] = $ptr - 1; } return $matches; @@ -164,7 +165,7 @@ private static function wordMatch(&$dict, $info, $glyphClassMarks, $ptr) $next = ord($dict[$dictptr + 1]); if ($next == self::INTERMEDIATE_MATCH || $next == self::FINAL_MATCH) { // Do not match if next character in text is a Mark - if (isset($info[$ptr]['uni']) && strpos($glyphClassMarks, $info[$ptr]['hex']) === false) { + if (isset($info[$ptr]['uni']) && !isset($marks[$info[$ptr]['hex']])) { $matches[] = $ptr - 1; } } diff --git a/src/TTFontFile.php b/src/TTFontFile.php index 9b9444b78..2c8c6632a 100644 --- a/src/TTFontFile.php +++ b/src/TTFontFile.php @@ -1359,7 +1359,7 @@ protected function restrictedFont() /** * GDEF's glyph lists as the parser keeps them: space-prefixed, "|"-separated hex, " 00641| 00642", - * which is what LookupFlag and the shaper search. + * which is what LookupFlag and the shaper read. * * @param string[] $glyphs One class, as hex * @@ -2762,7 +2762,7 @@ protected function gsubContextRule(array $Lookup, $i, $c, $tag, $scripttag, $ign $lookupGlyphs = $luss['Replace']; // Only where the nested lookup's (first) glyph is one the rule's position can hold - if (strpos($rule['input'][$seqIndex], $lookupGlyphs[0]) === false) { + if (!GlyphString::inList($rule['input'][$seqIndex], $lookupGlyphs[0])) { continue; } diff --git a/tests/Mpdf/ArabicContextEdgeTest.php b/tests/Mpdf/ArabicContextEdgeTest.php new file mode 100644 index 000000000..07c7c5417 --- /dev/null +++ b/tests/Mpdf/ArabicContextEdgeTest.php @@ -0,0 +1,90 @@ + 'utf-8', + 'fontDir' => [__DIR__ . '/../data/ttf'], + 'fontdata' => ['notosansarabiccontextedgesynthetic' => [ + 'R' => 'NotoSansArabic-ContextEdge-Synthetic.ttf', + 'useOTL' => 0xFF, + ]], + 'default_font' => 'notosansarabiccontextedgesynthetic', + ]); + $mpdf->WriteHTML('

' . $html . '

'); + + return array_values(unpack('N*', mb_convert_encoding($mpdf->drawnText[0], 'UTF-32BE', 'UTF-8'))); + } + + public function dataRuns() + { + return [ + 'a word ending in a mark, where the lookahead runs out' => [ + [self::BEH, self::BEH, self::FATHA], + [self::FATHA, self::BEH, self::BEH], + ], + 'a word starting with a mark, where the backtrack runs out' => [ + [self::FATHA, self::BEH, self::BEH], + [self::BEH, self::BEH, self::FATHA], + ], + 'the lookahead met past the mark' => [ + [self::BEH, self::BEH, self::FATHA, self::LOW_ALEF], + [self::LOW_ALEF, self::FATHA, self::DOTLESS_BEH_FINAL, self::BEH], + ], + 'the backtrack met past the mark' => [ + [self::LOW_ALEF, self::FATHA, self::BEH, self::BEH], + [self::BEH, self::DOTLESS_BEH_INITIAL, self::FATHA, self::LOW_ALEF], + ], + ]; + } + + /** + * @dataProvider dataRuns + */ + public function testAFormsContextIsMatchedUpToTheEdgeOfTheRun($codepoints, $expected) + { + $this->assertSame($expected, $this->drawn($codepoints)); + } + +} diff --git a/tests/Mpdf/Fixtures/arabic-shape.php b/tests/Mpdf/Fixtures/arabic-shape.php new file mode 100644 index 000000000..4d267e322 --- /dev/null +++ b/tests/Mpdf/Fixtures/arabic-shape.php @@ -0,0 +1,34 @@ + + */ + +require __DIR__ . '/../../../vendor/autoload.php'; + +set_time_limit(5); +error_reporting(E_ERROR); + +list($runs, $usetags, $marks) = json_decode(base64_decode($argv[1]), true); + +$forms = []; +foreach ($runs as $name => $run) { + list($hexes, $glyphs) = $run; + + $info = []; + foreach ($hexes as $hex) { + $info[] = ['hex' => $hex, 'uni' => hexdec($hex)]; + } + + \Mpdf\Shaper\Arabic::shape($info, $glyphs, $marks, $usetags, 'arab'); + + foreach ($info as $char) { + $forms[$name][] = [$char['hex'], $char['form']]; + } +} + +echo json_encode($forms); diff --git a/tests/Mpdf/Fonts/GlyphStringTest.php b/tests/Mpdf/Fonts/GlyphStringTest.php index db15d7432..97021a7c5 100644 --- a/tests/Mpdf/Fonts/GlyphStringTest.php +++ b/tests/Mpdf/Fonts/GlyphStringTest.php @@ -33,6 +33,48 @@ public function codepointProvider() ]; } + /** + * A six-digit glyph holds two five-digit ones: U+100300 is 10030 followed by a 0, and a 1 followed + * by 00300. Neither is in a class that names only U+100300. + * + * @dataProvider listProvider + */ + public function testAGlyphIsInAListOnlyWhereTheListNamesIt($glyphs, $glyph, $expected) + { + $set = GlyphString::set($glyphs); + + $this->assertSame($expected, isset($set[$glyph]), 'set()'); + $this->assertSame($expected, GlyphString::inList($glyphs, $glyph), 'inList()'); + } + + public function listProvider() + { + return [ + 'plane 16, itself' => [' 100300', '100300', true], + 'a BMP glyph its hex ends with' => [' 100300', '00300', false], + 'a plane 1 glyph its hex begins with' => [' 100300', '10030', false], + 'a plane 16 glyph whose hex ends with a BMP one in the list' => [' 00300', '100300', false], + 'the first of several' => [' 00300| 100300| 00302', '00300', true], + 'the last of several' => [' 00300| 100300| 00302', '00302', true], + 'between two that hold it' => [' 100300| 00300| 003001', '00300', true], + 'several, none of them it' => [' 00301| 100300| 00302', '00300', false], + 'an empty class' => ['', '00300', false], + 'a Coverage table, without the spaces' => ['00041|100300', '00300', false], + ]; + } + + /** + * inList() reads whatever separates the glyphs, since the lists it is asked about are not all + * GDEF's shape + */ + public function testAListMaySeparateItsGlyphsWithAnythingButAHexDigit() + { + $this->assertTrue(GlyphString::inList('0FE8E 0FE94 ', '0FE94')); + $this->assertFalse(GlyphString::inList('0FE8E 10FE94 ', '0FE94')); + $this->assertTrue(GlyphString::inList('((?:(?: 00300| 00301))*)', '00301')); + $this->assertFalse(GlyphString::inList('((?:(?: 100300| 100301))*)', '00301')); + } + /** * \Mpdf\unicode_hex() has been a public function of the Mpdf namespace since 5.7.1 and upstream * declares it in the same place, so anything outside the library that calls it has to keep diff --git a/tests/Mpdf/Fonts/Table/LookupFlagTest.php b/tests/Mpdf/Fonts/Table/LookupFlagTest.php index 91b8d31b1..41ec1527e 100644 --- a/tests/Mpdf/Fonts/Table/LookupFlagTest.php +++ b/tests/Mpdf/Fonts/Table/LookupFlagTest.php @@ -2,6 +2,8 @@ namespace Mpdf\Fonts\Table; +use Mpdf\Fonts\GlyphString; + class LookupFlagTest extends \Yoast\PHPUnitPolyfills\TestCases\TestCase { @@ -109,6 +111,27 @@ public function testAnUndefinedMarkFilteringSetIsRefused() $this->lookupFlag->skips(0x0018, '00041', 3); } + /** + * GlyphString::of() writes a plane 16 glyph six digits wide, and the five-digit glyph its hex ends + * with is not skipped with it + */ + public function testAGlyphWhoseHexEndsAPlaneSixteenMarksIsNotSkippedWithIt() + { + $marks = ' ' . implode('| ', [GlyphString::of(0x100300)]); + $flag = new LookupFlag('demo', [ + 'GlyphClassBases' => '', + 'GlyphClassMarks' => $marks, + 'GlyphClassLigatures' => '', + 'GlyphClassComponents' => '', + 'MarkGlyphSets' => [], + 'MarkAttachmentType' => [], + ]); + + $this->assertFalse($flag->skips(LookupFlag::IGNORE_MARKS, GlyphString::of(0x0300), '')); + $this->assertFalse($flag->skips(LookupFlag::IGNORE_MARKS, GlyphString::of(0x0301), '')); + $this->assertTrue($flag->skips(LookupFlag::IGNORE_MARKS, GlyphString::of(0x100300), '')); + } + /** * An empty class adds no separator of its own, but one it follows still gets its "|" - which is * how the ignore strings in the cached GSUB data have always read diff --git a/tests/Mpdf/GdefClassMembershipTest.php b/tests/Mpdf/GdefClassMembershipTest.php new file mode 100644 index 000000000..be061d391 --- /dev/null +++ b/tests/Mpdf/GdefClassMembershipTest.php @@ -0,0 +1,134 @@ +mpdf = new Mpdf([ + 'mode' => 'utf-8', + 'tempDir' => $tempDir, + 'fontDir' => [__DIR__ . '/../data/ttf'], + 'fontdata' => [self::FONTKEY => [ + 'R' => 'NotoSans-PlaneSixteenMark-Synthetic.ttf', + 'useOTL' => 0xFF, + ]], + 'default_font' => self::FONTKEY, + ]); + + $this->otl = new Otl($this->mpdf, new FontCache(new Cache($tempDir . '/mpdf/ttfontdata'))); + } + + protected function tear_down() + { + $this->mpdf->cleanup(); + + parent::tear_down(); + } + + public function testTheParserClassesThePlaneSixteenMarkAsThePrivateUseCharacterItMapsItTo() + { + $this->shape([0x41]); + + $this->assertSame(' 00301| 0E001', $this->otl->GDEFdata[self::FONTKEY]['GlyphClassMarks']); + } + + public function dataRuns() + { + return [ + 'IgnoreMarks does not skip U+0300, whose hex U+100300 ends with' => [ + [0x41, 0x0300], + [[0x41, 'C', null], [0x60, 'C', null]], + ], + 'a mark attaches to U+10030, whose hex U+100300 begins with' => [ + [0x10030, 0x0301], + [[0x10030, 'C', null], [0x0301, 'M', ['BaseWidth' => 639, 'XPlacement' => 300, 'YPlacement' => 0]]], + ], + ]; + } + + /** + * @dataProvider dataRuns + */ + public function testAGlyphIsNotTakenForTheMarkItsHexIsPartOf($codepoints, $expected) + { + // Before the shaper first reads GDEF, which it then keeps for the font + $this->otl->GDEFdata[self::FONTKEY] = [ + 'GlyphClassBases' => ' 00020| 00041| 00060| 0007E| 00300| 10030', + 'GlyphClassMarks' => ' 00301| 100300', + 'GlyphClassLigatures' => '', + 'GlyphClassComponents' => '', + 'MarkGlyphSets' => [], + 'MarkAttachmentType' => [], + ]; + + $this->assertSame($expected, $this->shape($codepoints)); + } + + /** + * @param int[] $codepoints + * + * @return array[] The codepoint, the group and the positioning of each glyph of the run + */ + private function shape($codepoints) + { + $text = ''; + foreach ($codepoints as $codepoint) { + $text .= UtfString::code2utf($codepoint); + } + $this->otl->applyOTL($text, 0xFF); + + $run = []; + foreach ($this->otl->OTLdata['char_data'] as $i => $character) { + $run[] = [ + $character['uni'], + $this->otl->OTLdata['group'][$i], + isset($this->otl->OTLdata['GPOSinfo'][$i]) ? $this->otl->OTLdata['GPOSinfo'][$i] : null, + ]; + } + + return $run; + } + +} diff --git a/tests/Mpdf/Shaper/ArabicTest.php b/tests/Mpdf/Shaper/ArabicTest.php index 17da48a0d..097cad861 100644 --- a/tests/Mpdf/Shaper/ArabicTest.php +++ b/tests/Mpdf/Shaper/ArabicTest.php @@ -479,17 +479,137 @@ public function testEveryJoiningTableEntryIsFiledUnderItsOwnCodepoint() } } + /** + * A form a chained rule gives is drawn where the text holds the glyphs the rule names, and a glyph + * whose hex is only part of one of theirs is not one of them: 00628 is inside 100628, and 0064E + * inside 10064E. + * + * @dataProvider dataContextNamingAPlaneSixteenGlyph + */ + public function testAFormsContextIsNotMetByAGlyphWhoseHexIsPartOfOneItNames($prel, $ignore, $hexes, $expected) + { + $glyphs = $this->glyphs(); + $glyphs[self::DAL] = ['D_ISOL', 'D_FINA', 'prel' => [1 => [$prel]], 'ignore' => [1 => $ignore]]; + + $this->assertSame($expected, $this->shape($hexes, self::ALL_FORMS, 'arab', self::FATHA, $glyphs)); + } + + public function dataContextNamingAPlaneSixteenGlyph() + { + return [ + 'the backtrack' => [ + '1' . self::BEH, + '()', + [self::BEH, self::DAL], + [['B_INIT', 2], [self::DAL, 0]], + ], + 'the glyphs the lookup skips' => [ + self::BEH, + '((?:(?: 1' . self::FATHA . '))*)', + [self::BEH, self::FATHA, self::DAL], + [['B_INIT', 2], [self::FATHA, 0], [self::DAL, 0]], + ], + ]; + } + + /** + * The walk over the glyphs a chained rule's lookup ignores read past the edge of the run: a notice + * for each glyph on PHP 7, and from PHP 8 a walk that never returned (#204). Here the notice is what + * fails, so a regression cannot hang the suite. + * + * @dataProvider dataContextWalkedToTheEdgeOfTheRun + */ + public function testAFormsContextWalkReadsNothingPastTheEdgeOfTheRun($hexes, $glyphs, $expected) + { + set_error_handler(function ($number, $message, $file, $line) { + throw new \ErrorException($message, 0, $number, $file, $line); + }); + + try { + $forms = $this->shape($hexes, self::ALL_FORMS, 'arab', self::FATHA, $glyphs); + } finally { + restore_error_handler(); + } + + $this->assertSame($expected, $forms); + } + + /** + * The same cases, all in one child process under a time limit, for a regression that reads past the + * edge without a notice. The limit is the child's own rather than a `timeout` around it, so it holds + * on Windows too, and the child reports nothing short of a fatal error, so a walk warning on every + * step cannot fill the stderr pipe and leave it blocked instead of timed out. + */ + public function testAFormsContextWalkReturnsAtTheEdgeOfTheRun() + { + $runs = []; + $expected = []; + foreach ($this->dataContextWalkedToTheEdgeOfTheRun() as $name => $case) { + $runs[$name] = [$case[0], $case[1]]; + $expected[$name] = $case[2]; + } + + // base64, because Windows argument quoting does not survive the JSON's double quotes + $arg = base64_encode(json_encode([$runs, self::ALL_FORMS, ' ' . self::FATHA])); + $command = escapeshellarg(PHP_BINARY) . ' -d display_errors=stderr ' + . escapeshellarg(__DIR__ . '/../Fixtures/arabic-shape.php') . ' ' . $arg; + + $process = proc_open($command, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes, null, null, ['bypass_shell' => true]); + $output = stream_get_contents($pipes[1]); + $errors = stream_get_contents($pipes[2]); + fclose($pipes[1]); + fclose($pipes[2]); + $this->assertSame(0, proc_close($process), $errors); + + $this->assertSame($expected, json_decode($output, true)); + } + + public function dataContextWalkedToTheEdgeOfTheRun() + { + // The forms are the presentation forms rather than names, because the run is left holding them + // and shape() reads each back as hex + $ignoreFatha = '((?:(?: ' . self::FATHA . '))*)'; + + return [ + 'a backtrack that runs out at the start of the run' => [ + [self::FATHA, self::DAL], + [self::DAL => ['0FEA9', '0FEAA', 'prel' => [0 => [self::BEH]], 'ignore' => [0 => $ignoreFatha]]], + [[self::FATHA, 0], [self::DAL, 0]], + ], + 'a lookahead that runs out at the end of the run' => [ + [self::BEH, self::FATHA], + [self::BEH => ['0FE8F', 'postl' => [0 => [self::DAL]], 'ignore' => [0 => $ignoreFatha]]], + [[self::BEH, 0], [self::FATHA, 0]], + ], + 'a lookahead whose second position runs out after the first is met' => [ + [self::BEH, self::FATHA, self::DAL, self::FATHA], + [self::BEH => ['0FE8F', '0FE90', '0FE91', 'postl' => [2 => [self::DAL, self::DAL]], 'ignore' => [2 => $ignoreFatha]]], + [[self::BEH, 0], [self::FATHA, 0], [self::DAL, 0], [self::FATHA, 0]], + ], + 'a backtrack met past the ignored glyphs' => [ + [self::BEH, self::FATHA, self::DAL], + [self::DAL => ['0FEA9', '0FEAA', 'prel' => [1 => [self::BEH]], 'ignore' => [1 => $ignoreFatha]]], + [[self::BEH, 0], [self::FATHA, 0], ['0FEAA', 1]], + ], + 'a lookahead met past the ignored glyphs' => [ + [self::BEH, self::FATHA, self::DAL], + [self::BEH => ['0FE8F', '0FE90', '0FE91', 'postl' => [2 => [self::DAL]], 'ignore' => [2 => $ignoreFatha]]], + [['0FE91', 2], [self::FATHA, 0], [self::DAL, 0]], + ], + ]; + } + /** * @return array one [hex, form] pair per character, in logical order */ - private function shape($hexes, $usetags = self::ALL_FORMS, $scriptTag = 'arab', $glyphClassMarks = self::FATHA) + private function shape($hexes, $usetags = self::ALL_FORMS, $scriptTag = 'arab', $glyphClassMarks = self::FATHA, $glyphs = null) { $info = []; foreach ($hexes as $hex) { $info[] = ['hex' => $hex, 'uni' => hexdec($hex)]; } - Arabic::shape($info, $this->glyphs(), ' ' . $glyphClassMarks, $usetags, $scriptTag); + Arabic::shape($info, $glyphs === null ? $this->glyphs() : $glyphs, ' ' . $glyphClassMarks, $usetags, $scriptTag); $forms = []; foreach ($info as $char) { diff --git a/tests/Mpdf/Shaper/LineBreakingTest.php b/tests/Mpdf/Shaper/LineBreakingTest.php index 53a365cd3..630f7e9d0 100644 --- a/tests/Mpdf/Shaper/LineBreakingTest.php +++ b/tests/Mpdf/Shaper/LineBreakingTest.php @@ -54,6 +54,18 @@ public function testAWordInTheDictionaryEndsAWord() $this->assertSame([1], $this->southEastAsian($dict, [0x0E17, 0x0E14, 0x0E2A, 0x0E2D])); } + /** + * A word does not end before a mark GDEF classes, and a character whose hex is only part of a + * mark's is not one: 00E2A is inside 100E2A + */ + public function testAWordDoesNotEndBeforeAMark() + { + $dict = $this->linear([0x17, 0x14]) . chr(0x04); + + $this->assertSame([], $this->southEastAsian($dict, [0x0E17, 0x0E14, 0x0E2A, 0x0E2D], GlyphString::set(' 00E2A'))); + $this->assertSame([1], $this->southEastAsian($dict, [0x0E17, 0x0E14, 0x0E2A, 0x0E2D], GlyphString::set(' 100E2A'))); + } + public function testTextThatIsNotInTheDictionaryEndsNoWord() { $dict = $this->linear([0x17, 0x14]) . chr(0x04); @@ -105,10 +117,10 @@ private function tibetan($unicode) /** * @return array the indexes marked as ending a word */ - private function southEastAsian($dict, $unicode) + private function southEastAsian($dict, $unicode, $marks = []) { $info = $this->info($unicode); - LineBreaking::southEastAsian($info, $dict, ''); + LineBreaking::southEastAsian($info, $dict, $marks); return $this->wordEnds($info); } diff --git a/tests/data/fontcache/NotoSans-PlaneSixteenMark-Synthetic.json b/tests/data/fontcache/NotoSans-PlaneSixteenMark-Synthetic.json new file mode 100644 index 000000000..2d48db0c9 --- /dev/null +++ b/tests/data/fontcache/NotoSans-PlaneSixteenMark-Synthetic.json @@ -0,0 +1,98 @@ +{ + "_": "Generated by composer fontcache:update. See tests/Mpdf/Fonts/ParserGoldenMaster.php.", + "fullName": "NotoSans-PlaneSixteenMark-Synthetic", + "mtx": { + "GSUBScriptLang": { + "DFLT": "DFLT ", + "latn": "DFLT " + }, + "GSUBFeatures": { + "DFLT": { + "DFLT": { + "ccmp": [ + 0 + ] + } + }, + "latn": { + "DFLT": { + "ccmp": [ + 0 + ] + } + } + }, + "GSUBLookups": [ + { + "Type": 1, + "Flag": 8, + "SubtableCount": 1, + "Subtables": [ + 62 + ], + "MarkFilteringSet": "" + } + ], + "GPOSScriptLang": { + "DFLT": "DFLT ", + "latn": "DFLT " + }, + "GPOSFeatures": { + "DFLT": { + "DFLT": { + "mark": [ + 0 + ] + } + }, + "latn": { + "DFLT": { + "mark": [ + 0 + ] + } + } + }, + "GPOSLookups": [ + { + "Type": 4, + "Flag": 0, + "SubtableCount": 1, + "Subtables": [ + 62 + ], + "MarkFilteringSet": "" + } + ], + "MarkGlyphSets": [], + "rtlPUAstr": "", + "haskernGPOS": false, + "hassmallcapsGSUB": false + }, + "cache": { + "GDEFdata.json": { + "GlyphClassBases": " 00020| 00041| 00060| 0007E| 00300| 10030", + "GlyphClassMarks": " 00301| 0E001", + "GlyphClassLigatures": "", + "GlyphClassComponents": "", + "MarkGlyphSets": [], + "MarkAttachmentType": [] + }, + "GPOS.dat": "108 bytes, sha256 c1668d8fb37c2532bd19f2435d2f11a6b4bf397ec03e2511fa4b0f2d2616cf87", + "GPOSdata.json": [ + [ + { + "769": 0 + } + ] + ], + "GSUB.dat": "74 bytes, sha256 7cf15c4f04eae9517b326ff0aedf14c27e790d0304901cdde3da390ffb0da577", + "GSUBdata.json": [ + [ + { + "768": 0 + } + ] + ] + } +} diff --git a/tests/data/fontcache/NotoSansArabic-ContextEdge-Synthetic.json b/tests/data/fontcache/NotoSansArabic-ContextEdge-Synthetic.json new file mode 100644 index 000000000..07d46f738 --- /dev/null +++ b/tests/data/fontcache/NotoSansArabic-ContextEdge-Synthetic.json @@ -0,0 +1,319 @@ +{ + "_": "Generated by composer fontcache:update. See tests/Mpdf/Fonts/ParserGoldenMaster.php.", + "fullName": "NotoSansArabicContextEdgeSynthetic-Regular", + "mtx": { + "GSUBScriptLang": { + "DFLT": "DFLT ", + "arab": "DFLT ", + "cyrl": "DFLT ", + "dev2": "DFLT ", + "grek": "DFLT ", + "latn": "DFLT " + }, + "GSUBFeatures": { + "DFLT": { + "DFLT": { + "init": [ + 0 + ], + "medi": [ + 1 + ], + "fina": [ + 2 + ] + } + }, + "arab": { + "DFLT": { + "init": [ + 0 + ], + "medi": [ + 1 + ], + "fina": [ + 2 + ] + } + }, + "cyrl": { + "DFLT": { + "init": [ + 0 + ], + "medi": [ + 1 + ], + "fina": [ + 2 + ] + } + }, + "dev2": { + "DFLT": { + "init": [ + 0 + ], + "medi": [ + 1 + ], + "fina": [ + 2 + ] + } + }, + "grek": { + "DFLT": { + "init": [ + 0 + ], + "medi": [ + 1 + ], + "fina": [ + 2 + ] + } + }, + "latn": { + "DFLT": { + "init": [ + 0 + ], + "medi": [ + 1 + ], + "fina": [ + 2 + ] + } + } + }, + "GSUBLookups": [ + { + "Type": 6, + "Flag": 8, + "SubtableCount": 1, + "Subtables": [ + 122 + ], + "MarkFilteringSet": "" + }, + { + "Type": 1, + "Flag": 0, + "SubtableCount": 1, + "Subtables": [ + 148 + ], + "MarkFilteringSet": "" + }, + { + "Type": 6, + "Flag": 8, + "SubtableCount": 1, + "Subtables": [ + 162 + ], + "MarkFilteringSet": "" + }, + { + "Type": 1, + "Flag": 0, + "SubtableCount": 1, + "Subtables": [ + 194 + ], + "MarkFilteringSet": "" + }, + { + "Type": 1, + "Flag": 0, + "SubtableCount": 1, + "Subtables": [ + 208 + ], + "MarkFilteringSet": "" + } + ], + "GPOSScriptLang": { + "DFLT": "DFLT ", + "arab": "DFLT ", + "cyrl": "DFLT ", + "dev2": "DFLT ", + "grek": "DFLT ", + "latn": "DFLT " + }, + "GPOSFeatures": { + "DFLT": { + "DFLT": { + "mark": [ + 0 + ], + "mkmk": [ + 1 + ] + } + }, + "arab": { + "DFLT": { + "mark": [ + 0 + ], + "mkmk": [ + 1 + ] + } + }, + "cyrl": { + "DFLT": { + "mark": [ + 0 + ], + "mkmk": [ + 1 + ] + } + }, + "dev2": { + "DFLT": { + "mark": [ + 0 + ], + "mkmk": [ + 1 + ] + } + }, + "grek": { + "DFLT": { + "mark": [ + 0 + ], + "mkmk": [ + 1 + ] + } + }, + "latn": { + "DFLT": { + "mark": [ + 0 + ], + "mkmk": [ + 1 + ] + } + } + }, + "GPOSLookups": [ + { + "Type": 4, + "Flag": 0, + "SubtableCount": 1, + "Subtables": [ + 102 + ], + "MarkFilteringSet": "" + }, + { + "Type": 6, + "Flag": 16, + "SubtableCount": 1, + "Subtables": [ + 194 + ], + "MarkFilteringSet": 0 + } + ], + "MarkGlyphSets": [ + " 0E005" + ], + "rtlPUAstr": "\\x{0E002}-\\x{0E004}", + "haskernGPOS": false, + "hassmallcapsGSUB": false + }, + "cache": { + "GDEFdata.json": { + "GlyphClassBases": " 00020| 00627| 008AD| 0E000| 0E001| 0E002| 0E003| 0E004", + "GlyphClassMarks": " 0064E| 0E005", + "GlyphClassLigatures": "", + "GlyphClassComponents": "", + "MarkGlyphSets": [ + " 0E005" + ], + "MarkAttachmentType": [] + }, + "GPOS.dat": "234 bytes, sha256 0f363351c5ea276d72ca75127977045024304d1f2b19b04e8cfded715c737a13", + "GPOSdata.json": [ + [ + { + "57349": 0 + } + ], + [ + { + "57349": 0 + } + ] + ], + "GSUB.arab.DFLT.json": { + "rtlSUB": { + "00628": { + "2": "0E004", + "prel": { + "2": [ + "008AD" + ] + }, + "ignore": { + "2": "((?:(?: 0064E| 0E005))*)", + "1": "((?:(?: 0064E| 0E005))*)" + }, + "3": "0E003", + "1": "0E002", + "postl": { + "1": [ + "008AD" + ] + } + } + }, + "finals": "0E002 ", + "rphf": [], + "half": [], + "pref": [], + "blwf": [], + "pstf": [] + }, + "GSUB.dat": "220 bytes, sha256 3ac7cb0c04aead3a9fc0c5f6c607b54931c16a81c5d5edb41654ceb5d58d593e", + "GSUBdata.json": [ + [ + { + "1576": 0 + } + ], + [ + { + "1576": 0 + } + ], + [ + { + "1576": 0 + } + ], + [ + { + "1576": 0 + } + ], + [ + { + "1576": 0 + } + ] + ] + } +} diff --git a/tests/data/otldump/NotoSans-PlaneSixteenMark-Synthetic.txt b/tests/data/otldump/NotoSans-PlaneSixteenMark-Synthetic.txt new file mode 100644 index 000000000..a73861ad4 --- /dev/null +++ b/tests/data/otldump/NotoSans-PlaneSixteenMark-Synthetic.txt @@ -0,0 +1,24 @@ +

GDEF table

+

Glyph classes

+

Glyph class 1

+
Base glyph (single character, spacing glyph)
+
A ` ~ ̀ 𐀰
+

Glyph class 3

+
Mark glyph (non-spacing combining glyph)
+
◌́ ◌
+

GSUB Tables

+

GSUB Scripts & Languages

+
+
DFLT
DFLT: ccmp
latn
DFLT: ccmp
+
+

GPOS Tables

+

GPOS Scripts & Languages

+
+
DFLT
DFLT: mark
latn
DFLT: mark
+
+ +=== detail: script latn language DFLT === +

GSUB Tables

+
Lookup #0 [tag: ccmp]
Ignoring: Mark Glyphs
Subtable #0
LookupType 1: Single Substitution Subtable
U+0300   ̀  » »   `  U+0060
+

GPOS Tables

+
Lookup #0 [tag: mark]
Subtable #0
LookupType 4: MarkToBase attachment
Marks: ◌́
Bases: 𐀰
Example(s): 𐀰́  
diff --git a/tests/data/otldump/NotoSansArabic-ContextEdge-Synthetic.txt b/tests/data/otldump/NotoSansArabic-ContextEdge-Synthetic.txt new file mode 100644 index 000000000..2388457a5 --- /dev/null +++ b/tests/data/otldump/NotoSansArabic-ContextEdge-Synthetic.txt @@ -0,0 +1,27 @@ +

GDEF table

+

Glyph classes

+

Glyph class 1

+
Base glyph (single character, spacing glyph)
+
ا ࢭ     
+

Glyph class 3

+
Mark glyph (non-spacing combining glyph)
+
◌َ ◌
+

Mark Glyph Sets

+

Mark Glyph Set class: 0

+
◌
+

GSUB Tables

+

GSUB Scripts & Languages

+
+
DFLT
DFLT: init medi fina
arab
DFLT: init medi fina
cyrl
DFLT: init medi fina
dev2
DFLT: init medi fina
grek
DFLT: init medi fina
latn
DFLT: init medi fina
+
+

GPOS Tables

+

GPOS Scripts & Languages

+
+
DFLT
DFLT: mark mkmk
arab
DFLT: mark mkmk
cyrl
DFLT: mark mkmk
dev2
DFLT: mark mkmk
grek
DFLT: mark mkmk
latn
DFLT: mark mkmk
+
+ +=== detail: script latn language DFLT === +

GSUB Tables

+
Lookup #0 [tag: init]
Ignoring: Mark Glyphs
Subtable #0
LookupType 6: Chaining Contextual Substitution Subtable
Format 3: Coverage-based Chaining Context Glyph Substitution
CONTEXT:
Backtrack #0: U+08AD
Input #0:  ب 
Substitution Position: 0
Lookup #3 [tag: init]
Subtable #0
LookupType 1: Single Substitution Subtable
U+0628   ب  » »     M+E004
Lookup #1 [tag: medi]
Subtable #0
LookupType 1: Single Substitution Subtable
U+0628   ب  » »     M+E003
Lookup #2 [tag: fina]
Ignoring: Mark Glyphs
Subtable #0
LookupType 6: Chaining Contextual Substitution Subtable
Format 3: Coverage-based Chaining Context Glyph Substitution
CONTEXT:
Input #0:  ب 
Lookahead #0: U+08AD
Substitution Position: 0
Lookup #4 [tag: fina]
Subtable #0
LookupType 1: Single Substitution Subtable
U+0628   ب  » »     M+E002
+

GPOS Tables

+
Lookup #0 [tag: mark]
Subtable #0
LookupType 4: MarkToBase attachment
Marks: ◌
Bases: ا      ࢭ
Example(s):    ا                  ࢭ  
Lookup #1 [tag: mkmk]
Ignoring: Marks outside Mark Glyph Set[0]
Subtable #0
LookupType 6: MarkToMark attachment
Marks: ◌
Bases: ◌
Example(s): ◌  
diff --git a/tests/data/shaping/NotoSans-PlaneSixteenMark-Synthetic.txt b/tests/data/shaping/NotoSans-PlaneSixteenMark-Synthetic.txt new file mode 100644 index 000000000..0848d5b42 --- /dev/null +++ b/tests/data/shaping/NotoSans-PlaneSixteenMark-Synthetic.txt @@ -0,0 +1,96 @@ +=== latin === +0xFF 0041 0056 0041 0054 0061 0072 => 0041 0056 0041 0054 0061 0072 group=CCCCCC +0x80 0041 0056 0041 0054 0061 0072 => 0041 0056 0041 0054 0061 0072 group=CCCCCC +0xFF +kern -liga 0041 0056 0041 0054 0061 0072 => 0041 0056 0041 0054 0061 0072 group=CCCCCC +=== cyrillic === +0xFF 0416 0430 0439 => 0416 0430 0439 group=CCC +0x80 0416 0430 0439 => 0416 0430 0439 group=CCC +0xFF +kern -liga 0416 0430 0439 => 0416 0430 0439 group=CCC +=== greek === +0xFF 03B1 03B2 03C2 => 03B1 03B2 03C2 group=CCC +0x80 03B1 03B2 03C2 => 03B1 03B2 03C2 group=CCC +0xFF +kern -liga 03B1 03B2 03C2 => 03B1 03B2 03C2 group=CCC +=== hiragana === +0xFF 3042 3043 3044 => 3042 3043 3044 group=CCC +0x80 3042 3043 3044 => 3042 3043 3044 group=CCC +0xFF +kern -liga 3042 3043 3044 => 3042 3043 3044 group=CCC +=== arabic === +0xFF 0628 0640 0645 0644 0627 => 0628 0640 0645 0644 0627 group=CCCCC gpos={"1":{"kashida":8}} +0x80 0628 0640 0645 0644 0627 => 0628 0640 0645 0644 0627 group=CCCCC gpos={"1":{"kashida":8}} +0xFF +kern -liga 0628 0640 0645 0644 0627 => 0628 0640 0645 0644 0627 group=CCCCC gpos={"1":{"kashida":8}} +=== syriac === +0xFF 0710 0712 0713 0715 => 0710 0712 0713 0715 group=CCCC +0x80 0710 0712 0713 0715 => 0710 0712 0713 0715 group=CCCC +0xFF +kern -liga 0710 0712 0713 0715 => 0710 0712 0713 0715 group=CCCC +=== nko === +0xFF 07CA 07CB 07CC => 07CA 07CB 07CC group=CCC +0x80 07CA 07CB 07CC => 07CA 07CB 07CC group=CCC +0xFF +kern -liga 07CA 07CB 07CC => 07CA 07CB 07CC group=CCC +=== devanagari === +0xFF 0915 094D 0937 093F => 0915 094D 093F 0937 group=CCCC +0x80 0915 094D 0937 093F => 0915 094D 093F 0937 group=CCCC +0xFF +kern -liga 0915 094D 0937 093F => 0915 094D 093F 0937 group=CCCC +=== bengali === +0xFF 0995 09CD 09B7 09BF => 0995 09CD 09BF 09B7 group=CCCC +0x80 0995 09CD 09B7 09BF => 0995 09CD 09BF 09B7 group=CCCC +0xFF +kern -liga 0995 09CD 09B7 09BF => 0995 09CD 09BF 09B7 group=CCCC +=== gurmukhi === +0xFF 0A15 0A4D 0A38 0A3F => 0A15 0A4D 0A3F 0A38 group=CCCC +0x80 0A15 0A4D 0A38 0A3F => 0A15 0A4D 0A3F 0A38 group=CCCC +0xFF +kern -liga 0A15 0A4D 0A38 0A3F => 0A15 0A4D 0A3F 0A38 group=CCCC +=== tamil === +0xFF 0B95 0BCD 0BB7 0BBF => 0B95 0BCD 0BB7 0BBF group=CCCC +0x80 0B95 0BCD 0BB7 0BBF => 0B95 0BCD 0BB7 0BBF group=CCCC +0xFF +kern -liga 0B95 0BCD 0BB7 0BBF => 0B95 0BCD 0BB7 0BBF group=CCCC +=== malayalam === +0xFF 0D15 0D4D 0D37 0D3F => 0D15 0D4D 0D37 0D3F group=CCCC +0x80 0D15 0D4D 0D37 0D3F => 0D15 0D4D 0D37 0D3F group=CCCC +0xFF +kern -liga 0D15 0D4D 0D37 0D3F => 0D15 0D4D 0D37 0D3F group=CCCC +=== sinhala === +0xFF 0D9A 0DCA 0DBB 0DBB => 0D9A 0DCA 0DBB 0DBB group=CCCC +0x80 0D9A 0DCA 0DBB 0DBB => 0D9A 0DCA 0DBB 0DBB group=CCCC +0xFF +kern -liga 0D9A 0DCA 0DBB 0DBB => 0D9A 0DCA 0DBB 0DBB group=CCCC +=== khmer === +0xFF 1780 17D2 1781 17C1 => 17C1 1780 17D2 1781 group=CCCC +0x80 1780 17D2 1781 17C1 => 17C1 1780 17D2 1781 group=CCCC +0xFF +kern -liga 1780 17D2 1781 17C1 => 17C1 1780 17D2 1781 group=CCCC +=== thai === +0xFF 0E01 0E34 0E48 0E23 => 0E01 0E34 0E48 0E23 group=CCCC +0x80 0E01 0E34 0E48 0E23 => 0E01 0E34 0E48 0E23 group=CCCC +0xFF +kern -liga 0E01 0E34 0E48 0E23 => 0E01 0E34 0E48 0E23 group=CCCC +=== lao === +0xFF 0E81 0EB4 0E8D => 0E81 0EB4 0E8D group=CCC +0x80 0E81 0EB4 0E8D => 0E81 0EB4 0E8D group=CCC +0xFF +kern -liga 0E81 0EB4 0E8D => 0E81 0EB4 0E8D group=CCC +=== myanmar === +0xFF 1000 103A 1039 1001 => 1000 103A 1039 1001 group=CCCC +0x80 1000 103A 1039 1001 => 1000 103A 1039 1001 group=CCCC +0xFF +kern -liga 1000 103A 1039 1001 => 1000 103A 1039 1001 group=CCCC +=== new tai lue === +0xFF 1980 19B0 1981 => 1980 19B0 1981 group=CCC +0x80 1980 19B0 1981 => 1980 19B0 1981 group=CCC +0xFF +kern -liga 1980 19B0 1981 => 1980 19B0 1981 group=CCC +=== cham === +0xFF AA00 AA33 AA01 => AA00 AA33 AA01 group=CCC +0x80 AA00 AA33 AA01 => AA00 AA33 AA01 group=CCC +0xFF +kern -liga AA00 AA33 AA01 => AA00 AA33 AA01 group=CCC +=== tai tham === +0xFF 1A20 1A60 1A21 => 1A20 1A60 1A21 group=CCC +0x80 1A20 1A60 1A21 => 1A20 1A60 1A21 group=CCC +0xFF +kern -liga 1A20 1A60 1A21 => 1A20 1A60 1A21 group=CCC +=== mixed scripts === +0xFF 0041 0628 0915 0042 => 0041 0628 0915 0042 group=CCCC +0x80 0041 0628 0915 0042 => 0041 0628 0915 0042 group=CCCC +0xFF +kern -liga 0041 0628 0915 0042 => 0041 0628 0915 0042 group=CCCC +=== spaced === +0xFF 0041 0020 0628 0020 0042 => 0041 0020 0628 0020 0042 group=CSCSC +0x80 0041 0020 0628 0020 0042 => 0041 0020 0628 0020 0042 group=CSCSC +0xFF +kern -liga 0041 0020 0628 0020 0042 => 0041 0020 0628 0020 0042 group=CSCSC +=== font characters 0 === +0xFF 0020 0041 0060 007E 0300 0301 E000 E001 => 0020 0041 0060 007E 0060 0301 E000 E001 group=SCCCCMCM +0x80 0020 0041 0060 007E 0300 0301 E000 E001 => 0020 0041 0060 007E 0300 0301 E000 E001 group=SCCCCMCM +0xFF +kern -liga 0020 0041 0060 007E 0300 0301 E000 E001 => 0020 0041 0060 007E 0060 0301 E000 E001 group=SCCCCMCM +=== font characters 1 === +0xFF 10030 => 10030 group=C +0x80 10030 => 10030 group=C +0xFF +kern -liga 10030 => 10030 group=C diff --git a/tests/data/shaping/NotoSansArabic-ContextEdge-Synthetic.txt b/tests/data/shaping/NotoSansArabic-ContextEdge-Synthetic.txt new file mode 100644 index 000000000..b22550262 --- /dev/null +++ b/tests/data/shaping/NotoSansArabic-ContextEdge-Synthetic.txt @@ -0,0 +1,96 @@ +=== latin === +0xFF 0041 0056 0041 0054 0061 0072 => 0041 0056 0041 0054 0061 0072 group=CCCCCC +0x80 0041 0056 0041 0054 0061 0072 => 0041 0056 0041 0054 0061 0072 group=CCCCCC +0xFF +kern -liga 0041 0056 0041 0054 0061 0072 => 0041 0056 0041 0054 0061 0072 group=CCCCCC +=== cyrillic === +0xFF 0416 0430 0439 => 0416 0430 0439 group=CCC +0x80 0416 0430 0439 => 0416 0430 0439 group=CCC +0xFF +kern -liga 0416 0430 0439 => 0416 0430 0439 group=CCC +=== greek === +0xFF 03B1 03B2 03C2 => 03B1 03B2 03C2 group=CCC +0x80 03B1 03B2 03C2 => 03B1 03B2 03C2 group=CCC +0xFF +kern -liga 03B1 03B2 03C2 => 03B1 03B2 03C2 group=CCC +=== hiragana === +0xFF 3042 3043 3044 => 3042 3043 3044 group=CCC +0x80 3042 3043 3044 => 3042 3043 3044 group=CCC +0xFF +kern -liga 3042 3043 3044 => 3042 3043 3044 group=CCC +=== arabic === +0xFF 0628 0640 0645 0644 0627 => 0628 0640 0645 0644 0627 group=CCCCC gpos={"1":{"kashida":8}} +0x80 0628 0640 0645 0644 0627 => 0628 0640 0645 0644 0627 group=CCCCC gpos={"1":{"kashida":8}} +0xFF +kern -liga 0628 0640 0645 0644 0627 => 0628 0640 0645 0644 0627 group=CCCCC gpos={"1":{"kashida":8}} +=== syriac === +0xFF 0710 0712 0713 0715 => 0710 0712 0713 0715 group=CCCC +0x80 0710 0712 0713 0715 => 0710 0712 0713 0715 group=CCCC +0xFF +kern -liga 0710 0712 0713 0715 => 0710 0712 0713 0715 group=CCCC +=== nko === +0xFF 07CA 07CB 07CC => 07CA 07CB 07CC group=CCC +0x80 07CA 07CB 07CC => 07CA 07CB 07CC group=CCC +0xFF +kern -liga 07CA 07CB 07CC => 07CA 07CB 07CC group=CCC +=== devanagari === +0xFF 0915 094D 0937 093F => 0915 094D 093F 0937 group=CCCC +0x80 0915 094D 0937 093F => 0915 094D 093F 0937 group=CCCC +0xFF +kern -liga 0915 094D 0937 093F => 0915 094D 093F 0937 group=CCCC +=== bengali === +0xFF 0995 09CD 09B7 09BF => 0995 09CD 09BF 09B7 group=CCCC +0x80 0995 09CD 09B7 09BF => 0995 09CD 09BF 09B7 group=CCCC +0xFF +kern -liga 0995 09CD 09B7 09BF => 0995 09CD 09BF 09B7 group=CCCC +=== gurmukhi === +0xFF 0A15 0A4D 0A38 0A3F => 0A15 0A4D 0A3F 0A38 group=CCCC +0x80 0A15 0A4D 0A38 0A3F => 0A15 0A4D 0A3F 0A38 group=CCCC +0xFF +kern -liga 0A15 0A4D 0A38 0A3F => 0A15 0A4D 0A3F 0A38 group=CCCC +=== tamil === +0xFF 0B95 0BCD 0BB7 0BBF => 0B95 0BCD 0BB7 0BBF group=CCCC +0x80 0B95 0BCD 0BB7 0BBF => 0B95 0BCD 0BB7 0BBF group=CCCC +0xFF +kern -liga 0B95 0BCD 0BB7 0BBF => 0B95 0BCD 0BB7 0BBF group=CCCC +=== malayalam === +0xFF 0D15 0D4D 0D37 0D3F => 0D15 0D4D 0D37 0D3F group=CCCC +0x80 0D15 0D4D 0D37 0D3F => 0D15 0D4D 0D37 0D3F group=CCCC +0xFF +kern -liga 0D15 0D4D 0D37 0D3F => 0D15 0D4D 0D37 0D3F group=CCCC +=== sinhala === +0xFF 0D9A 0DCA 0DBB 0DBB => 0D9A 0DCA 0DBB 0DBB group=CCCC +0x80 0D9A 0DCA 0DBB 0DBB => 0D9A 0DCA 0DBB 0DBB group=CCCC +0xFF +kern -liga 0D9A 0DCA 0DBB 0DBB => 0D9A 0DCA 0DBB 0DBB group=CCCC +=== khmer === +0xFF 1780 17D2 1781 17C1 => 17C1 1780 17D2 1781 group=CCCC +0x80 1780 17D2 1781 17C1 => 17C1 1780 17D2 1781 group=CCCC +0xFF +kern -liga 1780 17D2 1781 17C1 => 17C1 1780 17D2 1781 group=CCCC +=== thai === +0xFF 0E01 0E34 0E48 0E23 => 0E01 0E34 0E48 0E23 group=CCCC +0x80 0E01 0E34 0E48 0E23 => 0E01 0E34 0E48 0E23 group=CCCC +0xFF +kern -liga 0E01 0E34 0E48 0E23 => 0E01 0E34 0E48 0E23 group=CCCC +=== lao === +0xFF 0E81 0EB4 0E8D => 0E81 0EB4 0E8D group=CCC +0x80 0E81 0EB4 0E8D => 0E81 0EB4 0E8D group=CCC +0xFF +kern -liga 0E81 0EB4 0E8D => 0E81 0EB4 0E8D group=CCC +=== myanmar === +0xFF 1000 103A 1039 1001 => 1000 103A 1039 1001 group=CCCC +0x80 1000 103A 1039 1001 => 1000 103A 1039 1001 group=CCCC +0xFF +kern -liga 1000 103A 1039 1001 => 1000 103A 1039 1001 group=CCCC +=== new tai lue === +0xFF 1980 19B0 1981 => 1980 19B0 1981 group=CCC +0x80 1980 19B0 1981 => 1980 19B0 1981 group=CCC +0xFF +kern -liga 1980 19B0 1981 => 1980 19B0 1981 group=CCC +=== cham === +0xFF AA00 AA33 AA01 => AA00 AA33 AA01 group=CCC +0x80 AA00 AA33 AA01 => AA00 AA33 AA01 group=CCC +0xFF +kern -liga AA00 AA33 AA01 => AA00 AA33 AA01 group=CCC +=== tai tham === +0xFF 1A20 1A60 1A21 => 1A20 1A60 1A21 group=CCC +0x80 1A20 1A60 1A21 => 1A20 1A60 1A21 group=CCC +0xFF +kern -liga 1A20 1A60 1A21 => 1A20 1A60 1A21 group=CCC +=== mixed scripts === +0xFF 0041 0628 0915 0042 => 0041 0628 0915 0042 group=CCCC +0x80 0041 0628 0915 0042 => 0041 0628 0915 0042 group=CCCC +0xFF +kern -liga 0041 0628 0915 0042 => 0041 0628 0915 0042 group=CCCC +=== spaced === +0xFF 0041 0020 0628 0020 0042 => 0041 0020 0628 0020 0042 group=CSCSC +0x80 0041 0020 0628 0020 0042 => 0041 0020 0628 0020 0042 group=CSCSC +0xFF +kern -liga 0041 0020 0628 0020 0042 => 0041 0020 0628 0020 0042 group=CSCSC +=== font characters 0 === +0xFF 0020 0627 0628 064E 08AD E000 E001 E002 => 0020 0627 0628 064E 08AD E000 E001 E002 group=SCCMCCCC gpos={"7":{"kashida":2}} +0x80 0020 0627 0628 064E 08AD E000 E001 E002 => 0020 0627 0628 064E 08AD E000 E001 E002 group=SCCMCCCC gpos={"7":{"kashida":2}} +0xFF +kern -liga 0020 0627 0628 064E 08AD E000 E001 E002 => 0020 0627 0628 064E 08AD E000 E001 E002 group=SCCMCCCC gpos={"7":{"kashida":2}} +=== font characters 1 === +0xFF E003 E004 E005 => E003 E004 E005 group=CCM gpos={"2":{"BaseWidth":269,"XPlacement":31,"YPlacement":-3}} +0x80 E003 E004 E005 => E003 E004 E005 group=CCM gpos={"2":{"BaseWidth":269,"XPlacement":31,"YPlacement":-3}} +0xFF +kern -liga E003 E004 E005 => E003 E004 E005 group=CCM gpos={"2":{"BaseWidth":269,"XPlacement":31,"YPlacement":-3}} diff --git a/tests/data/subset/NotoSans-PlaneSixteenMark-Synthetic.json b/tests/data/subset/NotoSans-PlaneSixteenMark-Synthetic.json new file mode 100644 index 000000000..4a6a2b5cd --- /dev/null +++ b/tests/data/subset/NotoSans-PlaneSixteenMark-Synthetic.json @@ -0,0 +1,123 @@ +{ + "_": "Generated by composer subset:update. See tests/Mpdf/Fonts/SubsetGoldenMaster.php.", + "useOTL=0x00": { + "characters": 7, + "makeSubset": { + "bytes": 2164, + "sha256": "9df5bc841e7302bc88da8f6e0c81ea38d8eb42792ba2ccd7126fb027d78e0983", + "tables": { + "OS/2": "96 bytes, checksum 0x69905ECF, sha256 75ed87fcd39de51c848126876b746496217cb77b11eee5924c77b998d965d9ac", + "cmap": "100 bytes, checksum 0xFE1F06FA, sha256 89b421e2370643be66cb089780c9efde21469aa1c60b3ee16a2d081e9189a0cf", + "gasp": "8 bytes, checksum 0x00000010, sha256 4ca731f86ad506ac0e320283dc9461926346de3c4d91d539ea7f6620d3826940", + "glyf": "328 bytes, checksum 0x5592D924, sha256 3b52e8f22833280c9a01178b3f3bcd4f717bbede1d2623e04a92d32825f73f08", + "head": "54 bytes, checksum 0x24A01B35, sha256 95d0254baa52b5498baeea26b2a6490cbe3dc902ae8f4e1befa4853fbb1ea28e", + "hhea": "36 bytes, checksum 0x04C00164, sha256 d823c15753198902d40fae1bdcd1c4ba7a80550c231827159cca494bb717a265", + "hmtx": "32 bytes, checksum 0x0A4AFDAE, sha256 e776e083f28df155a71827b989e5393ef22cd22e13cbdfaa2f0fe11b1b44fe1e", + "loca": "18 bytes, checksum 0x018A0150, sha256 2521a73cf2e8c40ec15313572a9abf19e18bf835ab21348de614103c457fbf5d", + "maxp": "32 bytes, checksum 0x000D0026, sha256 5d94c82e0267eb8bda3ae2583f29931f361ca329e8d02805939b19b3931aafac", + "name": "1236 bytes, checksum 0x738E9F04, sha256 0be8b381ecfa265eb0a24147f3646b5c16593a6c1fd1dc297da6de27a6aab66f", + "post": "32 bytes, checksum 0xFF9F0032, sha256 4fdc83d5e42fe44650fe86f15a645d5cc926d07355ba945e1ede42737607bf83" + }, + "maxUni": 65535, + "defaultWidth": 600, + "codeToGlyph": "7 entries, sha256 49cb3701b52574d324fd3d5c45fc67c79d0d4fee83bef1338644e9609e6dee93" + }, + "makeSubsetSIP": { + "bytes": 2148, + "sha256": "4330ef41b9d6fd3c959c5fdfec254c97f8fd7ec4e4d12a0167bc865b91838a54", + "tables": { + "OS/2": "96 bytes, checksum 0x671AD6D3, sha256 dec08803ef4e2ce8a92937ef1ef3376b45e47c14eadf4d9fa09f8e2cff17cd8a", + "cmap": "84 bytes, checksum 0x00380090, sha256 b744253609e2a88d21149d2993bd74c60e813a46a40dbdd5fa6a10f27bd1defe", + "gasp": "8 bytes, checksum 0x00000010, sha256 4ca731f86ad506ac0e320283dc9461926346de3c4d91d539ea7f6620d3826940", + "glyf": "328 bytes, checksum 0x5592D924, sha256 3b52e8f22833280c9a01178b3f3bcd4f717bbede1d2623e04a92d32825f73f08", + "head": "54 bytes, checksum 0x24A01B35, sha256 4a5f7976b0744ab3b65b43814b3a447d73e934d4aa816f4413783aa8ea032088", + "hhea": "36 bytes, checksum 0x04C00164, sha256 d823c15753198902d40fae1bdcd1c4ba7a80550c231827159cca494bb717a265", + "hmtx": "32 bytes, checksum 0x0A4AFDAE, sha256 e776e083f28df155a71827b989e5393ef22cd22e13cbdfaa2f0fe11b1b44fe1e", + "loca": "18 bytes, checksum 0x018A0150, sha256 2521a73cf2e8c40ec15313572a9abf19e18bf835ab21348de614103c457fbf5d", + "maxp": "32 bytes, checksum 0x000D0026, sha256 5d94c82e0267eb8bda3ae2583f29931f361ca329e8d02805939b19b3931aafac", + "name": "1236 bytes, checksum 0x737F9F04, sha256 40411a81b7ac52b71e19743c2e4dcc41232a064ce59c61358d620beaf586cbc4", + "post": "32 bytes, checksum 0xFF9F0032, sha256 4fdc83d5e42fe44650fe86f15a645d5cc926d07355ba945e1ede42737607bf83" + }, + "maxUniChar": 65584, + "defaultWidth": 600 + }, + "repackageTTF": { + "bytes": 2340, + "sha256": "9a1a6d3d2d5e1d6b783aea845afbfc6f1313db2ce174ca9ef73ff721ccedd15d", + "tables": { + "OS/2": "96 bytes, checksum 0x69905ECF, sha256 75ed87fcd39de51c848126876b746496217cb77b11eee5924c77b998d965d9ac", + "cmap": "184 bytes, checksum 0x010F13B5, sha256 17340b2a2bbaf28b2e2188a32693e2b812ead19f148da6418ef0fa3113c4357c", + "gasp": "8 bytes, checksum 0x00000010, sha256 4ca731f86ad506ac0e320283dc9461926346de3c4d91d539ea7f6620d3826940", + "glyf": "408 bytes, checksum 0x9F0EEDB7, sha256 e0fd580e9734f5d8592419566f8b725646a2b580c8f66aa9c97951f7b39863c1", + "head": "54 bytes, checksum 0x24A01B35, sha256 79d057003d9ddcd22a6fdc391fb897a087eda6ecb45ccd4796e1e09f87c5ccce", + "hhea": "36 bytes, checksum 0x04C00166, sha256 806d71a0901d3be883cb46e8a1d5dcb02bbd37a507a71b6c6c4482aaffc72c40", + "hmtx": "40 bytes, checksum 0x0CCAFBC1, sha256 f94f691680abb9a22beaad6f14962449f9df4dd8d3a47b3cc939f10cdb66500c", + "loca": "22 bytes, checksum 0x025B021B, sha256 1bbc152cda174dc93d7f9b6ae1edd0a6f44d595706e5974f5809bab59382f519", + "maxp": "32 bytes, checksum 0x000F0026, sha256 42c6c6b6190f27b9917ae0e063a5d610b2880c2dd2f6361c7f6164560bf3e3dd", + "name": "1236 bytes, checksum 0x738E9F04, sha256 0be8b381ecfa265eb0a24147f3646b5c16593a6c1fd1dc297da6de27a6aab66f", + "post": "32 bytes, checksum 0xFF9F0032, sha256 4fdc83d5e42fe44650fe86f15a645d5cc926d07355ba945e1ede42737607bf83" + }, + "maxUni": 0 + } + }, + "useOTL=0xFF": { + "characters": 10, + "makeSubset": { + "bytes": 2284, + "sha256": "cd3731c556cb0701a5277b2c641ed4c126942547080992772e40c645f93460d9", + "tables": { + "OS/2": "96 bytes, checksum 0x69905ECF, sha256 75ed87fcd39de51c848126876b746496217cb77b11eee5924c77b998d965d9ac", + "cmap": "116 bytes, checksum 0xDE2CE717, sha256 3610cab263d1bdb4ff2b7722b9b6db0a5db18616f9a25615228c58fb503e2a73", + "gasp": "8 bytes, checksum 0x00000010, sha256 4ca731f86ad506ac0e320283dc9461926346de3c4d91d539ea7f6620d3826940", + "glyf": "420 bytes, checksum 0x15B47714, sha256 414ee1a5d24493556c3a669fc83b61ef2344b6283c7aab0e6ae468aacbe36a53", + "head": "54 bytes, checksum 0x24A01B35, sha256 6cf0b7eac9e40bd3a0ce31947b493a9e0297f053b91f886bda547330aed17bde", + "hhea": "36 bytes, checksum 0x04C00166, sha256 806d71a0901d3be883cb46e8a1d5dcb02bbd37a507a71b6c6c4482aaffc72c40", + "hmtx": "40 bytes, checksum 0x0CCAFBC1, sha256 f94f691680abb9a22beaad6f14962449f9df4dd8d3a47b3cc939f10cdb66500c", + "loca": "22 bytes, checksum 0x026A0226, sha256 c2f366d49bab7c6d720fab9efbfb8ec9b8a50f99377ee2cc992c0bceae701df1", + "maxp": "32 bytes, checksum 0x000F0026, sha256 42c6c6b6190f27b9917ae0e063a5d610b2880c2dd2f6361c7f6164560bf3e3dd", + "name": "1236 bytes, checksum 0x738E9F04, sha256 0be8b381ecfa265eb0a24147f3646b5c16593a6c1fd1dc297da6de27a6aab66f", + "post": "32 bytes, checksum 0xFF9F0032, sha256 4fdc83d5e42fe44650fe86f15a645d5cc926d07355ba945e1ede42737607bf83" + }, + "maxUni": 65535, + "defaultWidth": 600, + "codeToGlyph": "10 entries, sha256 11275dbcb4c91f864eeaf65226c51141d2bcdba705fb24dc0fedb967f611039c" + }, + "makeSubsetSIP": { + "bytes": 2188, + "sha256": "52a6a002203d815f5eefeaaf32bc43083671744ce0c0339abe09f8f7c60b185d", + "tables": { + "OS/2": "96 bytes, checksum 0x671AD6D6, sha256 dced49671d63709d875fd91b4b020468a64f1ad76edc59efede3cecb59714ce1", + "cmap": "98 bytes, checksum 0x0074008B, sha256 7069cba5b60ce066b70695505d4d3e25596e7b93baff0fada19904b447b3a2a0", + "gasp": "8 bytes, checksum 0x00000010, sha256 4ca731f86ad506ac0e320283dc9461926346de3c4d91d539ea7f6620d3826940", + "glyf": "348 bytes, checksum 0x5AF3D405, sha256 7e2f05e7a3e5408b7d917864cbbba4182bfd7447cd7e7d90fc42a96d98a005f8", + "head": "54 bytes, checksum 0x24A01B35, sha256 4b2c52a2191b38992a5fbb3d66c629d08cab89f6f7631fbc460bcf557aa9e418", + "hhea": "36 bytes, checksum 0x04C00165, sha256 c14499b68ed8904e1706fce621a52c3f6f4a287e795eec861b3ffe298ae7136a", + "hmtx": "36 bytes, checksum 0x0A4BFBC1, sha256 ddb26c4c0d71f1694fedd70c42748a63ddaf25a5685ef0a59c506d7e61d0b38b", + "loca": "20 bytes, checksum 0x018A01FE, sha256 b27c7908c9b2d0615b2b87b7549b482277e9c93b42815b873045373f84640e35", + "maxp": "32 bytes, checksum 0x000E0026, sha256 a06643819368fbe265136f0fd8b86f771ca43604583385456f3eaf051e5f5f28", + "name": "1236 bytes, checksum 0x737F9F04, sha256 40411a81b7ac52b71e19743c2e4dcc41232a064ce59c61358d620beaf586cbc4", + "post": "32 bytes, checksum 0xFF9F0032, sha256 4fdc83d5e42fe44650fe86f15a645d5cc926d07355ba945e1ede42737607bf83" + }, + "maxUniChar": 65584, + "defaultWidth": 600 + }, + "repackageTTF": { + "bytes": 2264, + "sha256": "b434debea8e6a9b4fdb54e401117691f1a66ef0afe5215292aef4f2eb235d0e3", + "tables": { + "OS/2": "96 bytes, checksum 0x69905ECF, sha256 75ed87fcd39de51c848126876b746496217cb77b11eee5924c77b998d965d9ac", + "cmap": "108 bytes, checksum 0xE0EBE44C, sha256 a9eb287f469b4e4d2252f7a4cb7e1e3dd00d3549a4a1319f02c0701641bd31ea", + "gasp": "8 bytes, checksum 0x00000010, sha256 4ca731f86ad506ac0e320283dc9461926346de3c4d91d539ea7f6620d3826940", + "glyf": "408 bytes, checksum 0x9F0EEDB7, sha256 e0fd580e9734f5d8592419566f8b725646a2b580c8f66aa9c97951f7b39863c1", + "head": "54 bytes, checksum 0x24A01B35, sha256 27a5a6281e2a988221d2882335a51feb915e61a6052c51f03aaeb8a7ef9c9cc6", + "hhea": "36 bytes, checksum 0x04C00166, sha256 806d71a0901d3be883cb46e8a1d5dcb02bbd37a507a71b6c6c4482aaffc72c40", + "hmtx": "40 bytes, checksum 0x0CCAFBC1, sha256 f94f691680abb9a22beaad6f14962449f9df4dd8d3a47b3cc939f10cdb66500c", + "loca": "22 bytes, checksum 0x025B021B, sha256 1bbc152cda174dc93d7f9b6ae1edd0a6f44d595706e5974f5809bab59382f519", + "maxp": "32 bytes, checksum 0x000F0026, sha256 42c6c6b6190f27b9917ae0e063a5d610b2880c2dd2f6361c7f6164560bf3e3dd", + "name": "1236 bytes, checksum 0x738E9F04, sha256 0be8b381ecfa265eb0a24147f3646b5c16593a6c1fd1dc297da6de27a6aab66f", + "post": "32 bytes, checksum 0xFF9F0032, sha256 4fdc83d5e42fe44650fe86f15a645d5cc926d07355ba945e1ede42737607bf83" + }, + "maxUni": 0 + } + } +} diff --git a/tests/data/subset/NotoSansArabic-ContextEdge-Synthetic.json b/tests/data/subset/NotoSansArabic-ContextEdge-Synthetic.json new file mode 100644 index 000000000..6e5a80249 --- /dev/null +++ b/tests/data/subset/NotoSansArabic-ContextEdge-Synthetic.json @@ -0,0 +1,129 @@ +{ + "_": "Generated by composer subset:update. See tests/Mpdf/Fonts/SubsetGoldenMaster.php.", + "useOTL=0x00": { + "characters": 6, + "makeSubset": { + "bytes": 1576, + "sha256": "dcdb96f57cd383eedf19465cdd6f6cdfac3e3eb7a1d109c9af772573a29b6ea4", + "tables": { + "OS/2": "96 bytes, checksum 0x83C35E30, sha256 c8d44e1e3e21744fb6447d75613e7d094658598ae11cd4ecd6ea51953585ed86", + "cmap": "100 bytes, checksum 0x0A15122E, sha256 01206629b9b1976eb2133f58b90acc80bd992c2eea0522dae67e318aa0b4dac8", + "gasp": "8 bytes, checksum 0x00000010, sha256 4ca731f86ad506ac0e320283dc9461926346de3c4d91d539ea7f6620d3826940", + "glyf": "348 bytes, checksum 0x4B193AFD, sha256 72af017879d7b5cb74705e75432c75cf858d4a91d8cc6c666297a05e40cf35f6", + "head": "54 bytes, checksum 0x273BB4EB, sha256 953af44a9ea0f49777df3b92e5801c83bdbb3f3c5e582361c5c10e62881dba63", + "hhea": "36 bytes, checksum 0x09D800EF, sha256 ebabb47c05c5494a10ab408895203118d43d45389ca43a4c8e36f5f3708f4e55", + "hmtx": "32 bytes, checksum 0x0CF9015E, sha256 3d78bf2b079ef8409631c4226d8441771b9cf4a9bbd474aa6f5fb01ab00a8af1", + "loca": "18 bytes, checksum 0x01580114, sha256 08d4157aa56b51232a60cdc5bec49509a2b55e0edafda07c36dc3711fe5f910e", + "maxp": "32 bytes, checksum 0x00100067, sha256 d7ee222097d2365abb6565f7458e7529d7d9a5c13b9d12b1c5813bd75c4d06e1", + "name": "602 bytes, checksum 0x351B4D7C, sha256 a3def774242f05f98ccd86071e838297b51d6977d62d5050843374956fa48320", + "post": "32 bytes, checksum 0xFF9F0032, sha256 4fdc83d5e42fe44650fe86f15a645d5cc926d07355ba945e1ede42737607bf83", + "prep": "7 bytes, checksum 0x68068C85, sha256 907c4106ff9989c1805827d5581bdf107a253b098cb71eb374f5e47c35604452" + }, + "maxUni": 65535, + "defaultWidth": 600, + "codeToGlyph": "6 entries, sha256 cd05832cc20763d2baec888f97c96504b92c9dd87781018003995892069fd4d0" + }, + "makeSubsetSIP": { + "bytes": 1560, + "sha256": "7977ab83391dc8ac1b598f1e9bf535f9d051a22b0e42680b8f907f093187a957", + "tables": { + "OS/2": "96 bytes, checksum 0x69A2D689, sha256 6d12188d8c750cd88b4abc92ca8f6b60d03839c85f9c99660729749453539cba", + "cmap": "82 bytes, checksum 0x00420076, sha256 f6d9593a66846b174499232d5c653827aca29fb4afa95c9d48a8b1a05f33bb84", + "gasp": "8 bytes, checksum 0x00000010, sha256 4ca731f86ad506ac0e320283dc9461926346de3c4d91d539ea7f6620d3826940", + "glyf": "348 bytes, checksum 0x4B193AFD, sha256 5cb26f14c78f967800052329067816047ff222ffc04cbc2d661399249d184bcb", + "head": "54 bytes, checksum 0x273BB4EB, sha256 3a7a54b8884ebe1bf346527bf67e96ebf138691f42f3e6ec487ed3afcc6ce789", + "hhea": "36 bytes, checksum 0x09D800EF, sha256 ebabb47c05c5494a10ab408895203118d43d45389ca43a4c8e36f5f3708f4e55", + "hmtx": "32 bytes, checksum 0x0CF9015E, sha256 702795e2dc162157c10308bbcf49a107d874b680074596789fd4951ef157c2b6", + "loca": "18 bytes, checksum 0x014C0112, sha256 8c32e6c969ca9367469dfb6601142a14bfea4db0b1974fd58937239b098f958e", + "maxp": "32 bytes, checksum 0x00100067, sha256 d7ee222097d2365abb6565f7458e7529d7d9a5c13b9d12b1c5813bd75c4d06e1", + "name": "602 bytes, checksum 0x35144D7C, sha256 9a414e77ed3ff545e08019d6eb0c1a54fa3874cc0737e4cac1843d94cc2549c7", + "post": "32 bytes, checksum 0xFF9F0032, sha256 4fdc83d5e42fe44650fe86f15a645d5cc926d07355ba945e1ede42737607bf83", + "prep": "7 bytes, checksum 0x68068C85, sha256 907c4106ff9989c1805827d5581bdf107a253b098cb71eb374f5e47c35604452" + }, + "maxUniChar": 65535, + "defaultWidth": 600 + }, + "repackageTTF": { + "bytes": 2076, + "sha256": "f985836fe626eee4bdae90aa75c296d01ef00d1ff1d3ba83540cdeda385061d5", + "tables": { + "OS/2": "96 bytes, checksum 0x83C35E30, sha256 c8d44e1e3e21744fb6447d75613e7d094658598ae11cd4ecd6ea51953585ed86", + "cmap": "80 bytes, checksum 0x176604B5, sha256 e6150b2b386d85b244823a2f1782890e4abb76097e8949950e045f68300847a4", + "gasp": "8 bytes, checksum 0x00000010, sha256 4ca731f86ad506ac0e320283dc9461926346de3c4d91d539ea7f6620d3826940", + "glyf": "712 bytes, checksum 0xA188F2CF, sha256 be0e31a63f04eca9c536c5a128bfb0af8305bd8b968093ac56201d9728ac9f40", + "head": "54 bytes, checksum 0x273BB4EB, sha256 f516b17a24d254b5893183e6e7db0e333666dda711ca9a853b2d5070179d854f", + "hhea": "36 bytes, checksum 0x09D800F3, sha256 6ca77e5fbd56daf3cfb1b6fa175777fc37efb2dee3484e9b4eafc9b013685f37", + "hmtx": "48 bytes, checksum 0x14C501C4, sha256 beb829ee0b3b845cb6e98594d9558a6718caa5a6a5d0c51d3c14156ccc575788", + "loca": "26 bytes, checksum 0x04D2041D, sha256 7d7cfa93d31416192329dad07396fa208fb36dea393e3ec49d552e916798d08b", + "maxp": "32 bytes, checksum 0x00140067, sha256 9cbbb1d75753b1ffc75c0c0b2bc62dbe39970d0952d3cafaba7080450a4909d1", + "name": "602 bytes, checksum 0x351B4D7C, sha256 a3def774242f05f98ccd86071e838297b51d6977d62d5050843374956fa48320", + "post": "161 bytes, checksum 0xA62A664B, sha256 5b8d5360786a810d8f5eb32a72054f9c8a81ffe63c5ce9dd6ad9f96a6082da2d", + "prep": "7 bytes, checksum 0x68068C85, sha256 907c4106ff9989c1805827d5581bdf107a253b098cb71eb374f5e47c35604452" + }, + "maxUni": 0 + } + }, + "useOTL=0xFF": { + "characters": 12, + "makeSubset": { + "bytes": 1992, + "sha256": "6a0802412d24a3261579faacf2e1638afea571cc1fe668d9e44f7f90a084e983", + "tables": { + "OS/2": "96 bytes, checksum 0x83C35E30, sha256 c8d44e1e3e21744fb6447d75613e7d094658598ae11cd4ecd6ea51953585ed86", + "cmap": "116 bytes, checksum 0xEA2DF256, sha256 858fa3d0b930efbfd42a1a02aff15a6a53e516696c1f7bc7643f9112b9cb0b09", + "gasp": "8 bytes, checksum 0x00000010, sha256 4ca731f86ad506ac0e320283dc9461926346de3c4d91d539ea7f6620d3826940", + "glyf": "724 bytes, checksum 0x106683EE, sha256 9b7c70ea6d174f85dde0974c87cdad53be0ed647e330f09244f2aac062db1454", + "head": "54 bytes, checksum 0x273BB4EB, sha256 28ed64f53c5f8f7f20a77dc0441beba115080a6772e3b37aca7f8bacd9020fa6", + "hhea": "36 bytes, checksum 0x09D800F3, sha256 6ca77e5fbd56daf3cfb1b6fa175777fc37efb2dee3484e9b4eafc9b013685f37", + "hmtx": "48 bytes, checksum 0x14C501C4, sha256 beb829ee0b3b845cb6e98594d9558a6718caa5a6a5d0c51d3c14156ccc575788", + "loca": "26 bytes, checksum 0x04EA0432, sha256 ea45c3bbe7f5968b94088c5ac2fcef60612554631d8b53d2c4603ca73ac20598", + "maxp": "32 bytes, checksum 0x00140067, sha256 9cbbb1d75753b1ffc75c0c0b2bc62dbe39970d0952d3cafaba7080450a4909d1", + "name": "602 bytes, checksum 0x351B4D7C, sha256 a3def774242f05f98ccd86071e838297b51d6977d62d5050843374956fa48320", + "post": "32 bytes, checksum 0xFF9F0032, sha256 4fdc83d5e42fe44650fe86f15a645d5cc926d07355ba945e1ede42737607bf83", + "prep": "7 bytes, checksum 0x68068C85, sha256 907c4106ff9989c1805827d5581bdf107a253b098cb71eb374f5e47c35604452" + }, + "maxUni": 65535, + "defaultWidth": 600, + "codeToGlyph": "12 entries, sha256 5ce80ef51cc1a796dc4aface13e6f0653c3d26b58e6d2741578f9daf9c817bfe" + }, + "makeSubsetSIP": { + "bytes": 1972, + "sha256": "331771ee04bc7484c65904868479d2e1fed985fcafe3a017f356bfffacc112f7", + "tables": { + "OS/2": "96 bytes, checksum 0x69A2D68F, sha256 fd2a99f2195a1e56bb4e59d20af2b5720421965a32625525a1c755330d490c8e", + "cmap": "94 bytes, checksum 0x006000B5, sha256 75724bdb0aad5c8bd651f10a19b526bbc16e46dd6bce95ecbe992a14008013d8", + "gasp": "8 bytes, checksum 0x00000010, sha256 4ca731f86ad506ac0e320283dc9461926346de3c4d91d539ea7f6620d3826940", + "glyf": "724 bytes, checksum 0x106983F0, sha256 6b70d49a1702b20d6542845a3da8b2916299b00c6d4403e98f4efad27f65385b", + "head": "54 bytes, checksum 0x273BB4EB, sha256 f713df5e403f19107633d9f312e2f67b49ea083792e67fa5fbcfc9ee1515f5f7", + "hhea": "36 bytes, checksum 0x09D800F3, sha256 6ca77e5fbd56daf3cfb1b6fa175777fc37efb2dee3484e9b4eafc9b013685f37", + "hmtx": "48 bytes, checksum 0x14C501C4, sha256 878b29797358aa6ba5b230946534b0f7f7de475126f154c144cee1d37866e512", + "loca": "26 bytes, checksum 0x03F2034C, sha256 5b9e234c7070bf60ac5622aa013fabe4d9e43cbdf2d879a11095dcfd3bc3ebc8", + "maxp": "32 bytes, checksum 0x00140067, sha256 9cbbb1d75753b1ffc75c0c0b2bc62dbe39970d0952d3cafaba7080450a4909d1", + "name": "602 bytes, checksum 0x35144D7C, sha256 9a414e77ed3ff545e08019d6eb0c1a54fa3874cc0737e4cac1843d94cc2549c7", + "post": "32 bytes, checksum 0xFF9F0032, sha256 4fdc83d5e42fe44650fe86f15a645d5cc926d07355ba945e1ede42737607bf83", + "prep": "7 bytes, checksum 0x68068C85, sha256 907c4106ff9989c1805827d5581bdf107a253b098cb71eb374f5e47c35604452" + }, + "maxUniChar": 65535, + "defaultWidth": 600 + }, + "repackageTTF": { + "bytes": 2104, + "sha256": "c6820acff4d4cdfd8fe7b052ab1620e76a6b3970f8210316437a3d8d30fe9b28", + "tables": { + "OS/2": "96 bytes, checksum 0x83C35E30, sha256 c8d44e1e3e21744fb6447d75613e7d094658598ae11cd4ecd6ea51953585ed86", + "cmap": "108 bytes, checksum 0xECAFEFC8, sha256 fb705da3b20a90b7cda468287c78e3ae892a6894c7ae4bb018c5a3196efc9666", + "gasp": "8 bytes, checksum 0x00000010, sha256 4ca731f86ad506ac0e320283dc9461926346de3c4d91d539ea7f6620d3826940", + "glyf": "712 bytes, checksum 0xA188F2CF, sha256 be0e31a63f04eca9c536c5a128bfb0af8305bd8b968093ac56201d9728ac9f40", + "head": "54 bytes, checksum 0x273BB4EB, sha256 69365b1031c0ea9b54f8880029e54cea827e5d51a0acc915ebdb46e0bb5da720", + "hhea": "36 bytes, checksum 0x09D800F3, sha256 6ca77e5fbd56daf3cfb1b6fa175777fc37efb2dee3484e9b4eafc9b013685f37", + "hmtx": "48 bytes, checksum 0x14C501C4, sha256 beb829ee0b3b845cb6e98594d9558a6718caa5a6a5d0c51d3c14156ccc575788", + "loca": "26 bytes, checksum 0x04D2041D, sha256 7d7cfa93d31416192329dad07396fa208fb36dea393e3ec49d552e916798d08b", + "maxp": "32 bytes, checksum 0x00140067, sha256 9cbbb1d75753b1ffc75c0c0b2bc62dbe39970d0952d3cafaba7080450a4909d1", + "name": "602 bytes, checksum 0x351B4D7C, sha256 a3def774242f05f98ccd86071e838297b51d6977d62d5050843374956fa48320", + "post": "161 bytes, checksum 0xA62A664B, sha256 5b8d5360786a810d8f5eb32a72054f9c8a81ffe63c5ce9dd6ad9f96a6082da2d", + "prep": "7 bytes, checksum 0x68068C85, sha256 907c4106ff9989c1805827d5581bdf107a253b098cb71eb374f5e47c35604452" + }, + "maxUni": 0 + } + } +} diff --git a/tests/data/ttf/NotoSans-PlaneSixteenMark-Synthetic.ttf b/tests/data/ttf/NotoSans-PlaneSixteenMark-Synthetic.ttf new file mode 100644 index 000000000..bfc4e9a05 Binary files /dev/null and b/tests/data/ttf/NotoSans-PlaneSixteenMark-Synthetic.ttf differ diff --git a/tests/data/ttf/NotoSansArabic-ContextEdge-Synthetic.ttf b/tests/data/ttf/NotoSansArabic-ContextEdge-Synthetic.ttf new file mode 100644 index 000000000..63407de00 Binary files /dev/null and b/tests/data/ttf/NotoSansArabic-ContextEdge-Synthetic.ttf differ