Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions src/Mpdf.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand All @@ -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;
Expand Down
189 changes: 189 additions & 0 deletions tests/Mpdf/FontdataMetricOverrideTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
<?php

namespace Mpdf;

/**
* The Ascent, Descent and Leading a fontdata entry sets replace the ones read from the font, for layout and in the
* FontDescriptor, without being written into the metrics cache the next document reads
*/
class FontdataMetricOverrideTest extends \Yoast\PHPUnitPolyfills\TestCases\TestCase
{

use PageStreams;

/** In points */
const FONT_SIZE = 11;

/** What DejaVu Sans itself declares */
private static $fontMetrics = ['Ascent' => 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('<p>Line one<br>Line two<br>Line three</p>');

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];
}

}
144 changes: 144 additions & 0 deletions tests/Mpdf/TtcFontIdTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
<?php

namespace Mpdf;

use Mpdf\Fonts\BlobReader;
use Mpdf\Fonts\TableWriter;

/**
* The TTCfontID a fontdata entry sets picks which font of a TrueType Collection is registered and embedded
*/
class TtcFontIdTest extends \Yoast\PHPUnitPolyfills\TestCases\TestCase
{

use PageStreams;

/** Holds the collection and the cache the documents write */
private $tempDir;

/**
* Pack two of the fixture fonts into a collection, in a directory of its own
*/
protected function set_up()
{
parent::set_up();

$this->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('<p>abc</p>');

$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;
}

}
Loading