diff --git a/src/Mpdf.php b/src/Mpdf.php index 5f60029d4..b273eca7b 100644 --- a/src/Mpdf.php +++ b/src/Mpdf.php @@ -4039,13 +4039,13 @@ private function registerFont($fontkey, $family, array $font) // Override with values from config_font.php if (isset($this->fontdata[$family]['Ascent']) && $this->fontdata[$family]['Ascent']) { - $desc['Ascent'] = $this->fontdata[$family]['Ascent']; + $font['desc']['Ascent'] = $this->fontdata[$family]['Ascent']; } if (isset($this->fontdata[$family]['Descent']) && $this->fontdata[$family]['Descent']) { - $desc['Descent'] = $this->fontdata[$family]['Descent']; + $font['desc']['Descent'] = $this->fontdata[$family]['Descent']; } if (isset($this->fontdata[$family]['Leading']) && $this->fontdata[$family]['Leading']) { - $desc['Leading'] = $this->fontdata[$family]['Leading']; + $font['desc']['Leading'] = $this->fontdata[$family]['Leading']; } $i = count($this->fonts) + $this->extraFontSubsets + 1; @@ -4173,7 +4173,7 @@ public function fontMetrics($family, $style) $ttffile = $this->fontFileFinder->findFontFile($this->fontdata[$family][$stylekey]); $ttfstat = stat($ttffile); - $TTCfontID = isset($this->fontdata[$family]['TTCfontID'][$stylekey]) ? isset($this->fontdata[$family]['TTCfontID'][$stylekey]) : 0; + $TTCfontID = isset($this->fontdata[$family]['TTCfontID'][$stylekey]) ? $this->fontdata[$family]['TTCfontID'][$stylekey] : 0; $fontUseOTL = isset($this->fontdata[$family]['useOTL']) ? $this->fontdata[$family]['useOTL'] : false; $BMPonly = in_array($family, $this->BMPonly) ? true : false; @@ -4196,6 +4196,12 @@ public function fontMetrics($family, $style) $regenerate = true; } // mPDF 6 + /* The cache may hold another font of the same collection. Compared as integers because caches + * written from v7.1.8 until #321 stored true for every configured TTCfontID, and true == 2 */ + if ((int) (isset($font['TTCfontID']) ? $font['TTCfontID'] : 0) !== (int) $TTCfontID) { + $regenerate = true; + } + // A cache written by a release that laid its files out differently cannot be read by this one if (!MetricsGenerator::isCurrent($font)) { $regenerate = true; diff --git a/tests/Mpdf/FontdataMetricOverrideTest.php b/tests/Mpdf/FontdataMetricOverrideTest.php new file mode 100644 index 000000000..b2dbe30c9 --- /dev/null +++ b/tests/Mpdf/FontdataMetricOverrideTest.php @@ -0,0 +1,189 @@ + 928, 'Descent' => -236, 'Leading' => 0]; + + private static $overrides = ['Ascent' => 1500, 'Descent' => -900, 'Leading' => 400]; + + /** A test's documents share it, so the second one reads the cache the first one wrote */ + private $tempDir; + + /** + * Give each test a cache of its own + */ + protected function set_up() + { + parent::set_up(); + + $this->tempDir = sys_get_temp_dir() . '/mpdf-fontdata-metric-override-' . uniqid('', true); + } + + /** + * Remove the cache the documents wrote + */ + protected function tear_down() + { + /* The directories a Cache makes, innermost first, then the tempDir itself */ + foreach ([$this->tempDir . '/mpdf/ttfontdata', $this->tempDir . '/mpdf', $this->tempDir] as $directory) { + if (!is_dir($directory)) { + continue; + } + + foreach (glob($directory . '/*') as $entry) { + if (is_file($entry)) { + unlink($entry); + } + } + + rmdir($directory); + } + + parent::tear_down(); + } + + /** + * The registered font carries each override, and the font's own value for any metric left alone + * + * @dataProvider overrideProvider + * + * @param array $overrides + */ + public function testOverridesReplaceTheFontMetrics(array $overrides) + { + $mpdf = $this->document($overrides); + + $this->assertSame(array_merge(self::$fontMetrics, $overrides), $this->metrics($mpdf->fonts['dejavusans']['desc'])); + } + + /** + * Each override alone, and all three together + * + * @return array + */ + public function overrideProvider() + { + return [ + 'Ascent' => [['Ascent' => 1500]], + 'Descent' => [['Descent' => -900]], + 'Leading' => [['Leading' => 400]], + 'all three' => [self::$overrides], + ]; + } + + /** + * A normal line is adjustFontDescLineheight times Ascent - Descent + Leading, with the Leading added again as a + * line gap, so the lines of a paragraph move apart by exactly what the overrides imply + */ + public function testOverridesSetTheLineHeight() + { + $plain = $this->document([]); + $overridden = $this->document(self::$overrides); + + $this->assertEqualsWithDelta($this->pitch(self::$fontMetrics, $plain), $this->linePitch($plain), 0.002); + $this->assertEqualsWithDelta($this->pitch(self::$overrides, $overridden), $this->linePitch($overridden), 0.002); + } + + /** + * The embedded font describes itself with the overridden metrics, as it did before they were lost + */ + public function testOverridesReachTheFontDescriptor() + { + $pdf = $this->output($this->document(self::$overrides)); + + $this->assertMatchesRegularExpression('/\/FontDescriptor\s+\/FontName \/\w+\+DejaVuSans\s+[^>]*\/Ascent 1500\s+\/Descent -900\s+\/Leading 400\s/', $pdf); + } + + /** + * A document without overrides that reads the cache a document with them wrote gets the font's own metrics + */ + public function testOverridesStayOutOfTheCache() + { + $this->output($this->document(self::$overrides)); + + $cached = json_decode(file_get_contents($this->tempDir . '/mpdf/ttfontdata/dejavusans.mtx.json'), true); + $this->assertSame(self::$fontMetrics, $this->metrics($cached['desc'])); + + $plain = $this->document([]); + $this->assertSame(self::$fontMetrics, $this->metrics($plain->fonts['dejavusans']['desc'])); + } + + /** + * A document in DejaVu Sans, with $overrides on its fontdata entry, and three lines written + * + * @param array $overrides + * + * @return Mpdf + */ + private function document(array $overrides) + { + $mpdf = new Mpdf([ + 'mode' => 'utf-8', + 'tempDir' => $this->tempDir, + 'default_font' => 'dejavusans', + 'default_font_size' => self::FONT_SIZE, + 'fontdata' => ['dejavusans' => ['R' => 'DejaVuSans.ttf'] + $overrides], + ]); + $mpdf->compress = false; + $mpdf->WriteHTML('

Line one
Line two
Line three

'); + + return $mpdf; + } + + /** + * @param array $desc + * + * @return array Ascent, Descent and Leading from a font's desc, in that order + */ + private function metrics(array $desc) + { + return [ + 'Ascent' => $desc['Ascent'], + 'Descent' => $desc['Descent'], + 'Leading' => $desc['Leading'], + ]; + } + + /** + * @param array $metrics + * @param Mpdf $mpdf + * + * @return float The distance between baselines, in points, that a normal line height gives these metrics + */ + private function pitch(array $metrics, Mpdf $mpdf) + { + $lineHeight = $mpdf->adjustFontDescLineheight * ($metrics['Ascent'] - $metrics['Descent'] + $metrics['Leading']) / 1000; + + return ($lineHeight + $metrics['Leading'] / 1000) * self::FONT_SIZE; + } + + /** + * @param Mpdf $mpdf + * + * @return float The distance between baselines, in points, of the three lines drawn, which must be even + */ + private function linePitch(Mpdf $mpdf) + { + preg_match_all('/BT [\d.]+ ([\d.]+) Td/', $this->output($mpdf), $matches); + $baselines = array_map('floatval', $matches[1]); + + $this->assertCount(3, $baselines); + $this->assertEqualsWithDelta($baselines[0] - $baselines[1], $baselines[1] - $baselines[2], 0.002); + + return $baselines[0] - $baselines[1]; + } + +} diff --git a/tests/Mpdf/TtcFontIdTest.php b/tests/Mpdf/TtcFontIdTest.php new file mode 100644 index 000000000..7b3b102db --- /dev/null +++ b/tests/Mpdf/TtcFontIdTest.php @@ -0,0 +1,144 @@ +tempDir = sys_get_temp_dir() . '/mpdf-ttc-font-id-' . uniqid('', true); + mkdir($this->tempDir); + + $fixtures = __DIR__ . '/../data/ttf/'; + file_put_contents($this->tempDir . '/Pair.ttc', $this->collection([ + file_get_contents($fixtures . 'NotoSans-GPOS3-Synthetic.ttf'), + file_get_contents($fixtures . 'Carlito-MarkAttachmentType-Subset.ttf'), + ])); + } + + /** + * Remove the collection and the cache + */ + protected function tear_down() + { + $files = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->tempDir, \FilesystemIterator::SKIP_DOTS), \RecursiveIteratorIterator::CHILD_FIRST); + foreach ($files as $file) { + $file->isDir() ? rmdir($file->getPathname()) : unlink($file->getPathname()); + } + rmdir($this->tempDir); + + parent::tear_down(); + } + + /** + * The face registered, and the one embedded, is the one TTCfontID names + * + * @dataProvider faceProvider + * + * @param int $TTCfontID + * @param string $name The PostScript name of that face + */ + public function testTheConfiguredFaceIsTheOneUsed($TTCfontID, $name) + { + $this->assertFace($TTCfontID, $name); + } + + /** + * Each face of the collection, with its PostScript name + * + * @return array + */ + public function faceProvider() + { + return [ + 'the first face' => [1, 'NotoSans-Regular'], + 'the second face' => [2, 'Carlito-Regular'], + ]; + } + + /** + * Metrics cached for one face are not reused for another, which is also what catches a cache written while every + * configured TTCfontID was read as the first face + */ + public function testTheCacheOfAnotherFaceIsNotReused() + { + $this->assertFace(1, 'NotoSans-Regular'); + $this->assertFace(2, 'Carlito-Regular'); + } + + /** + * Render a document in the collection's face $TTCfontID and assert it is the face named $name + * + * @param int $TTCfontID + * @param string $name + */ + private function assertFace($TTCfontID, $name) + { + $mpdf = new Mpdf([ + 'mode' => 'utf-8', + 'tempDir' => $this->tempDir, + 'fontDir' => [$this->tempDir], + 'default_font' => 'pair', + 'fontdata' => ['pair' => ['R' => 'Pair.ttc', 'TTCfontID' => ['R' => $TTCfontID]]], + ]); + $mpdf->compress = false; + $mpdf->WriteHTML('

abc

'); + + $this->assertSame($TTCfontID, $mpdf->fonts['pair']['TTCfontID']); + $this->assertSame($name, $mpdf->fonts['pair']['name']); + $this->assertMatchesRegularExpression('/\/FontDescriptor\s+\/FontName \/[A-Z]{6}\+' . $name . '\s/', $this->output($mpdf)); + } + + /** + * A TrueType Collection of $fonts, each whole font copied in with its table offsets moved to where it now starts. + * Nothing is shared between them, which the format allows but does not require. + * + * @param string[] $fonts + * + * @return string + */ + private function collection(array $fonts) + { + $header = 'ttcf' . TableWriter::uint32(0x00010000) . TableWriter::uint32(count($fonts)); + $start = strlen($header) + 4 * count($fonts); + $body = ''; + + foreach ($fonts as $font) { + $header .= TableWriter::uint32($start); + + $reader = new BlobReader($font); + $reader->seek(4); + $numTables = $reader->readUInt16(); + + for ($i = 0; $i < $numTables; $i++) { + $record = 12 + 16 * $i + 8; + $reader->seek($record); + $font = TableWriter::replace($font, $record, TableWriter::uint32($reader->readUInt32() + $start)); + } + + $font .= str_repeat("\0", (4 - strlen($font) % 4) % 4); + $body .= $font; + $start += strlen($font); + } + + return $header . $body; + } + +}