diff --git a/src/Mpdf.php b/src/Mpdf.php index 965c12a83..521ec2723 100644 --- a/src/Mpdf.php +++ b/src/Mpdf.php @@ -25776,7 +25776,7 @@ function SubstituteCharsNonCore(&$writehtml_a, &$writehtml_i, &$writehtml_e) foreach ($unicode as $c => $char) { - if (($char > 127 || ($flag == 1 && $char == 32)) && $char != 173 && (!isset($this->subArrMB['a'][$char]) || ($flag == 1 && $char == 32)) && ($char < 1536 || ($char > 1791 && $char < 2304) || $char > 3455)) { + if ($char > 127 && $char != 173 && !isset($this->subArrMB['a'][$char]) && ($char < 1536 || ($char > 1791 && $char < 2304) || $char > 3455)) { if ($flag == 0) { $start = $c; } @@ -25803,7 +25803,8 @@ function SubstituteCharsNonCore(&$writehtml_a, &$writehtml_i, &$writehtml_e) foreach ($this->backupSubsFont as $bsfctr => $bsf) { - if ($this->fonttrans[$bsf] == 'chelvetica' || $this->fonttrans[$bsf] == 'ctimes' || $this->fonttrans[$bsf] == 'ccourier') { + // A backup font that is a core font's name draws nothing a core font does not + if (isset($this->fonttrans[$bsf]) && in_array($this->fonttrans[$bsf], ['chelvetica', 'ctimes', 'ccourier'], true)) { continue; } @@ -25880,25 +25881,30 @@ function SubstituteCharsMB(&$writehtml_a, &$writehtml_i, &$writehtml_e) $ftype = ''; $u = []; // Every emoji is outside ASCII, a keycap's U+20E3 included - $emojiClusters = preg_match('/[^\x00-\x7F]/', $writehtml_e) ? Emoji::clusters($unicode) : []; + $mayHaveEmoji = preg_match('/[^\x00-\x7F]/', $writehtml_e) === 1; + // Each emoji up to where the run ends, by where it starts: found as the scan reaches it, since the + // rest of the text is scanned again on the next pass + $emojiClusters = []; $codepointCount = count($unicode); // Not foreach, so the loop can jump past the rest of a multi-codepoint sequence once it has been handled for ($c = 0; $c < $codepointCount; $c++) { $char = $unicode[$c]; + $cluster = $mayHaveEmoji ? Emoji::clusterAt($unicode, $c, $codepointCount) : null; // An emoji moves whole or not at all, so its joiners and selectors go with it to the font // whose GSUB joins them - if (isset($emojiClusters[$c])) { + if ($cluster !== null) { // An emoji ends a SIP run; it is handled on the next pass if ($flag == 2) { break; } - $length = $emojiClusters[$c][0]; + $emojiClusters[$c] = $cluster; + $length = $cluster[0]; $emoji = array_slice($unicode, $c, $length); - if ($this->fontSubstitution->emojiWantsAnotherFont($emoji, $emojiClusters[$c][1])) { + if ($this->fontSubstitution->emojiWantsAnotherFont($emoji, $cluster[1])) { if ($flag == 0) { $start = $c; } @@ -25912,7 +25918,7 @@ function SubstituteCharsMB(&$writehtml_a, &$writehtml_i, &$writehtml_e) continue; } - if (($flag == 0 || $flag == 2) && (!$this->_charDefined($cw, $char) || ($flag == 2 && $char == 32)) && $this->checkSIP && $char > 131071) { // Unicode Plane 2 (SIP) + if (($flag == 0 || $flag == 2) && !$this->_charDefined($cw, $char) && $this->checkSIP && $char > 131071) { // Unicode Plane 2 (SIP) if (in_array($this->FontFamily, $this->available_CJK_fonts)) { return 0; @@ -25925,9 +25931,7 @@ function SubstituteCharsMB(&$writehtml_a, &$writehtml_i, &$writehtml_e) $flag = 2; $u[] = $char; - // elseif (($flag == 0 || $flag==1) && $char != 173 && !$this->_charDefined($cw,$char) && ($char<1423 || ($char>3583 && $char < 11263))) { - - } elseif (($flag == 0 || $flag == 1) && $char != 173 && (!$this->_charDefined($cw, $char) || ($flag == 1 && $char == 32)) && ($char < 1536 || ($char > 1791 && $char < 2304) || $char > 3455)) { + } elseif (($flag == 0 || $flag == 1) && $char != 173 && !$this->_charDefined($cw, $char) && ($char < 1536 || ($char > 1791 && $char < 2304) || $char > 3455)) { if ($flag == 0) { $start = $c; diff --git a/src/Unicode/Emoji.php b/src/Unicode/Emoji.php index f5945972d..c31f5792b 100644 --- a/src/Unicode/Emoji.php +++ b/src/Unicode/Emoji.php @@ -253,13 +253,16 @@ public static function isTag($codepoint) } /** + * The emoji at one place in a run, for a caller walking the run itself that would otherwise find + * every emoji in it with clusters() before it knows how far it will look + * * @param int[] $codepoints The run - * @param int $i Where in it to look + * @param int $i Where in it to look, which is not inside an emoji that starts earlier * @param int $count How long it is * * @return array|null [length, presentation] of the emoji starting at $i, or null where none does */ - private static function clusterAt(array $codepoints, $i, $count) + public static function clusterAt(array $codepoints, $i, $count) { $first = $codepoints[$i]; diff --git a/tests/Mpdf/EmojiSubstitutionTest.php b/tests/Mpdf/EmojiSubstitutionTest.php index f7ba32a3b..351f6acdf 100644 --- a/tests/Mpdf/EmojiSubstitutionTest.php +++ b/tests/Mpdf/EmojiSubstitutionTest.php @@ -89,10 +89,37 @@ public function testAKeycapGoesToTheFontThatHasAllOfIt() */ public function testAFlagAndASkinToneStayWithTheEmojiTheyQualify() { - $pieces = $this->drawn([0x1F1E6, 0x1F1FA, 0x20, 0x1F44D, 0x1F3FD], ['coloremoji']); + $pieces = $this->drawn([0x1F1E6, 0x1F1FA, 0x1F44D, 0x1F3FD], ['coloremoji']); + $this->assertCount(1, $pieces); + $this->assertSame('coloremoji', $pieces[0][0]); + $this->assertCount(2, $pieces[0][1], 'the flag and the thumb'); + } + + /** + * A space between two emoji is the document font's, which has one, and not carried into the run + * of the font the emoji go to: an emoji font's space can be as wide as an emoji, as Noto Color + * Emoji's is. The recorder leaves out a piece that is only a space, so the space shows as the two + * runs either side of it. + */ + public function testASpaceBetweenEmojiStaysInTheDocumentFont() + { + $pieces = $this->drawn([0x1F1E6, 0x1F1FA, 0x20, 0x1F44D], ['coloremoji']); + + $this->assertCount(2, $pieces, 'a run either side of the space'); $this->assertSame('coloremoji', $pieces[0][0]); - $this->assertCount(3, $pieces[0][1], 'the flag, the space the run carries, and the thumb'); + $this->assertCount(1, $pieces[0][1], 'the flag, without the space'); + $this->assertSame(['coloremoji', [0x1F44D]], $pieces[1]); + } + + /** + * A space in a core font's text is the core font's too, and ends the run a backup font draws + */ + public function testASpaceBetweenWordsACoreFontLacksStaysInTheCoreFont() + { + $pieces = $this->drawn([0x61, 0x416, 0x416, 0x20, 0x416, 0x62], ['dejavusans'], ['default_font' => 'chelvetica']); + + $this->assertSame([['chelvetica', [0x61]], ['dejavusans', [0x416, 0x416]], ['dejavusans', [0x416]], ['chelvetica', [0x62]]], $pieces); } /** diff --git a/tests/data/snapshots/color-emoji-cbdt.pdf b/tests/data/snapshots/color-emoji-cbdt.pdf index 859c193ec..e4126b53b 100644 Binary files a/tests/data/snapshots/color-emoji-cbdt.pdf and b/tests/data/snapshots/color-emoji-cbdt.pdf differ diff --git a/tests/data/snapshots/color-emoji-colrv0.pdf b/tests/data/snapshots/color-emoji-colrv0.pdf index f6aa10fb2..2d88362dd 100644 Binary files a/tests/data/snapshots/color-emoji-colrv0.pdf and b/tests/data/snapshots/color-emoji-colrv0.pdf differ diff --git a/tests/data/snapshots/color-emoji-sbix.pdf b/tests/data/snapshots/color-emoji-sbix.pdf index 676b8d4bd..6c1d2e2fb 100644 Binary files a/tests/data/snapshots/color-emoji-sbix.pdf and b/tests/data/snapshots/color-emoji-sbix.pdf differ