<?php
namespace Mpdf\Writer;
use Mpdf\Strict;
use Mpdf\Fonts\FontCache;
use Mpdf\Mpdf;
use Mpdf\TTFontFile;
class FontWriter
{
use Strict;
/**
* @var \Mpdf\Mpdf
*/
private $mpdf;
/**
* @var \Mpdf\Writer\BaseWriter
*/
private $writer;
/**
* @var \Mpdf\Fonts\FontCache
*/
private $fontCache;
/**
* @var string
*/
private $fontDescriptor;
public function __construct(Mpdf $mpdf, BaseWriter $writer, FontCache $fontCache, $fontDescriptor)
{
$this->mpdf = $mpdf;
$this->writer = $writer;
$this->fontCache = $fontCache;
$this->fontDescriptor = $fontDescriptor;
}
public function writeFonts()
{
foreach ($this->mpdf->FontFiles as $fontkey => $info) {
// TrueType embedded
if (isset($info['type']) && $info['type'] === 'TTF' && !$info['sip'] && !$info['smp']) {
$used = true;
$asSubset = false;
foreach ($this->mpdf->fonts as $k => $f) {
if (isset($f['fontkey']) && $f['fontkey'] === $fontkey && $f['type'] === 'TTF') {
$used = $f['used'];
if ($used) {
$nChars = (ord($f['cw'][0]) << 8) + ord($f['cw'][1]);
$usage = (int) (count($f['subset']) * 100 / $nChars);
$fsize = $info['length1'];
// Always subset the very large TTF files
if ($fsize > ($this->mpdf->maxTTFFilesize * 1024)) {
$asSubset = true;
} elseif ($usage < $this->mpdf->percentSubset) {
$asSubset = true;
}
}
if ($this->mpdf->PDFA || $this->mpdf->PDFX) {
$asSubset = false;
}
$this->mpdf->fonts[$k]['asSubset'] = $asSubset;
break;
}
}
if ($used && !$asSubset) {
// Font file embedding
$this->writer->object();
$this->mpdf->FontFiles[$fontkey]['n'] = $this->mpdf->n;
$originalsize = $info['length1'];
if ($this->mpdf->repackageTTF || $this->mpdf->fonts[$fontkey]['TTCfontID'] > 0 || $this->mpdf->fonts[$fontkey]['useOTL'] > 0) { // mPDF 5.7.1
// First see if there is a cached compressed file
if ($this->fontCache->has($fontkey . '.ps.z') && $this->fontCache->jsonHas($fontkey . '.ps.json')) {
$font = $this->fontCache->load($fontkey . '.ps.z');
$originalsize = $this->fontCache->jsonLoad($fontkey . '.ps.json'); // sets $originalsize (of repackaged font)
} else {
$ttf = new TTFontFile($this->fontCache, $this->fontDescriptor);
$font = $ttf->repackageTTF($this->mpdf->FontFiles[$fontkey]['ttffile'], $this->mpdf->fonts[$fontkey]['TTCfontID'], $this->mpdf->debugfonts, $this->mpdf->fonts[$fontkey]['useOTL']); // mPDF 5.7.1
$originalsize = strlen($font);
$font = gzcompress($font);
unset($ttf);
$this->fontCache->binaryWrite($fontkey . '.ps.z', $font);
$this->fontCache->jsonWrite($fontkey . '.ps.json', $originalsize);
}
} elseif ($this->fontCache->has($fontkey . '.z')) {
$font = $this->fontCache->load($fontkey . '.z');
} else {
$font = file_get_contents($this->mpdf->FontFiles[$fontkey]['ttffile']);
$font = gzcompress($font);
$this->fontCache->binaryWrite($fontkey . '.z', $font);
}
$this->writer->write('<</Length ' . strlen($font));
$this->writer->write('/Filter /FlateDecode');
$this->writer->write('/Length1 ' . $originalsize);
$this->writer->write('>>');
$this->writer->stream($font);
$this->writer->write('endobj');
}
}
}
foreach ($this->mpdf->fonts as $k => $font) {
// Font objects
$type = $font['type'];
$name = $font['name'];
if ($type === 'TTF' && (!isset($font['used']) || !$font['used'])) {
continue;
}
// @log Writing fonts
if (isset($font['asSubset'])) {
$asSubset = $font['asSubset'];
} else {
$asSubset = '';
}
if ($type === 'Type0') { // Adobe CJK Fonts
$this->mpdf->fonts[$k]['n'] = $this->mpdf->n + 1;
$this->writer->object();
$this->writer->write('<</Type /Font');
$this->writeType0($font);
} elseif ($type === 'core') {
// Standard font
$this->mpdf->fonts[$k]['n'] = $this->mpdf->n + 1;
if ($this->mpdf->PDFA || $this->mpdf->PDFX) {
throw new \Mpdf\MpdfException('Core fonts are not allowed in PDF/A1-b or PDFX/1-a files (Times, Helvetica, Courier etc.)');
}
$this->writer->object();
$this->writer->write('<</Type /Font');
$this->writer->write('/BaseFont /' . $name);
$this->writer->write('/Subtype /Type1');
if ($name !== 'Symbol' && $name !== 'ZapfDingbats') {
$this->writer->write('/Encoding /WinAnsiEncoding');
}
$this->writer->write('>>');
$this->writer->write('endobj');
} elseif ($type === 'TTF' && ($font['sip'] || $font['smp'])) {
// TrueType embedded SUBSETS for SIP (CJK extB containing Supplementary Ideographic Plane 2)
// Or Unicode Plane 1 - Supplementary Multilingual Plane
if (!$font['used']) {
continue;
}
$ssfaid = 'AA';
$ttf = new TTFontFile($this->fontCache, $this->fontDescriptor);
$subsetCount = count($font['subsetfontids']);
for ($sfid = 0; $sfid < $subsetCount; $sfid++) {
$this->mpdf->fonts[$k]['n'][$sfid] = $this->mpdf->n + 1; // NB an array for subset
$subsetname = 'MPDF' . $ssfaid . '+' . $font['name'];
$ssfaid++;
/* For some strange reason a subset ($sfid > 0) containing less than 97 characters causes an error
so fill up the array */
for ($j = count($font['subsets'][$sfid]); $j < 98; $j++) {
$font['subsets'][$sfid][$j] = 0;
}
$subset = $font['subsets'][$sfid];
unset($subset[0]);
$ttfontstream = $ttf->makeSubsetSIP($font['ttffile'], $subset, $font['TTCfontID'], $this->mpdf->debugfonts, $font['useOTL']); // mPDF 5.7.1
$ttfontsize = strlen($ttfontstream);
$fontstream = gzcompress($ttfontstream);
$widthstring = '';
$toUnistring = '';
foreach ($font['subsets'][$sfid] as $cp => $u) {
$w = $this->mpdf->_getCharWidth($font['cw'], $u);
if ($w !== false) {
$widthstring .= $w . ' ';
} else {
$widthstring .= round($ttf->defaultWidth) . ' ';
}
if ($u > 65535) {
$utf8 = chr(($u >> 18) + 240) . chr((($u >> 12) & 63) + 128) . chr((($u >> 6) & 63) + 128) . chr(($u & 63) + 128);
$utf16 = mb_convert_encoding($utf8, 'UTF-16BE', 'UTF-8');
$l1 = ord($utf16[0]);
$h1 = ord($utf16[1]);
$l2 = ord($utf16[2]);
$h2 = ord($utf16[3]);
$toUnistring .= sprintf("<%02s> <%02s%02s%02s%02s>\n", strtoupper(dechex($cp)), strtoupper(dechex($l1)), strtoupper(dechex($h1)), strtoupper(dechex($l2)), strtoupper(dechex($h2)));
} else {
$toUnistring .= sprintf("<%02s> <%04s>\n", strtoupper(dechex($cp)), strtoupper(dechex($u)));
}
}
// Additional Type1 or TrueType font
$this->writer->object();
$this->writer->write('<</Type /Font');
$this->writer->write('/BaseFont /' . $subsetname);
$this->writer->write('/Subtype /TrueType');
$this->writer->write('/FirstChar 0 /LastChar ' . (count($font['subsets'][$sfid]) - 1));
$this->writer->write('/Widths ' . ($this->mpdf->n + 1) . ' 0 R');
$this->writer->write('/FontDescriptor ' . ($this->mpdf->n + 2) . ' 0 R');
$this->writer->write('/ToUnicode ' . ($this->mpdf->n + 3) . ' 0 R');
$this->writer->write('>>');
$this->writer->write('endobj');
// Widths
$this->writer->object();
$this->writer->write('[' . $widthstring . ']');
$this->writer->write('endobj');
// Descriptor
$this->writer->object();
$s = '<</Type /FontDescriptor /FontName /' . $subsetname . "\n";
foreach ($font['desc'] as $kd => $v) {
if ($kd === 'Flags') {
$v |= 4;
$v &= ~32;
} // SYMBOLIC font flag
$s .= ' /' . $kd . ' ' . $v . "\n";
}
$s .= '/FontFile2 ' . ($this->mpdf->n + 2) . ' 0 R';
$this->writer->write($s . '>>');
$this->writer->write('endobj');
// ToUnicode
$this->writer->object();
$toUni = "/CIDInit /ProcSet findresource begin\n";
$toUni .= "12 dict begin\n";
$toUni .= "begincmap\n";
$toUni .= "/CIDSystemInfo\n";
$toUni .= "<</Registry (Adobe)\n";
$toUni .= "/Ordering (UCS)\n";
$toUni .= "/Supplement 0\n";
$toUni .= ">> def\n";
$toUni .= "/CMapName /Adobe-Identity-UCS def\n";
$toUni .= "/CMapType 2 def\n";
$toUni .= "1 begincodespacerange\n";
$toUni .= "<00> <FF>\n";
// $toUni .= sprintf("<00> <%02s>\n", strtoupper(dechex(count($font['subsets'][$sfid])-1)));
$toUni .= "endcodespacerange\n";
$toUni .= count($font['subsets'][$sfid]) . " beginbfchar\n";
$toUni .= $toUnistring;
$toUni .= "endbfchar\n";
$toUni .= "endcmap\n";
$toUni .= "CMapName currentdict /CMap defineresource pop\n";
$toUni .= "end\n";
$toUni .= "end\n";
$this->writer->write('<</Length ' . strlen($toUni) . '>>');
$this->writer->stream($toUni);
$this->writer->write('endobj');
// Font file
$this->writer->object();
$this->writer->write('<</Length ' . strlen($fontstream));
$this->writer->write('/Filter /FlateDecode');
$this->writer->write('/Length1 ' . $ttfontsize);
$this->writer->write('>>');
$this->writer->stream($fontstream);
$this->writer->write('endobj');
} // foreach subset
unset($ttf);
} elseif ($type === 'TTF') { // TrueType embedded SUBSETS or FULL
$this->mpdf->fonts[$k]['n'] = $this->mpdf->n + 1;
if ($asSubset) {
$ssfaid = 'A';
$ttf = new TTFontFile($this->fontCache, $this->fontDescriptor);
$fontname = 'MPDFA' . $ssfaid . '+' . $font['name'];
$subset = $font['subset'];
unset($subset[0]);
$ttfontstream = $ttf->makeSubset($font['ttffile'], $subset, $font['TTCfontID'], $this->mpdf->debugfonts, $font['useOTL']);
$ttfontsize = strlen($ttfontstream);
$fontstream = gzcompress($ttfontstream);
$codeToGlyph = $ttf->codeToGlyph;
unset($codeToGlyph[0]);
} else {
$fontname = $font['name'];
}
// Type0 Font
// A composite font - a font composed of other fonts, organized hierarchically
$this->writer->object();
$this->writer->write('<</Type /Font');
$this->writer->write('/Subtype /Type0');
$this->writer->write('/BaseFont /' . $fontname . '');
$this->writer->write('/Encoding /Identity-H');
$this->writer->write('/DescendantFonts [' . ($this->mpdf->n + 1) . ' 0 R]');
$this->writer->write('/ToUnicode ' . ($this->mpdf->n + 2) . ' 0 R');
$this->writer->write('>>');
$this->writer->write('endobj');
// CIDFontType2
// A CIDFont whose glyph descriptions are based on TrueType font technology
$this->writer->object();
$this->writer->write('<</Type /Font');
$this->writer->write('/Subtype /CIDFontType2');
$this->writer->write('/BaseFont /' . $fontname . '');
$this->writer->write('/CIDSystemInfo ' . ($this->mpdf->n + 2) . ' 0 R');
$this->writer->write('/FontDescriptor ' . ($this->mpdf->n + 3) . ' 0 R');
if (isset($font['desc']['MissingWidth'])) {
$this->writer->write('/DW ' . $font['desc']['MissingWidth'] . '');
}
if (!$asSubset && $this->fontCache->has($font['fontkey'] . '.cw')) {
$w = $this->fontCache->load($font['fontkey'] . '.cw');
$this->writer->write($w);
} else {
$this->writeTTFontWidths($font, $asSubset, ($asSubset ? $ttf->maxUni : 0));
}
$this->writer->write('/CIDToGIDMap ' . ($this->mpdf->n + 4) . ' 0 R');
$this->writer->write('>>');
$this->writer->write('endobj');
// ToUnicode
$this->writer->object();
$toUni = "/CIDInit /ProcSet findresource begin\n";
$toUni .= "12 dict begin\n";
$toUni .= "begincmap\n";
$toUni .= "/CIDSystemInfo\n";
$toUni .= "<</Registry (Adobe)\n";
$toUni .= "/Ordering (UCS)\n";
$toUni .= "/Supplement 0\n";
$toUni .= ">> def\n";
$toUni .= "/CMapName /Adobe-Identity-UCS def\n";
$toUni .= "/CMapType 2 def\n";
$toUni .= "1 begincodespacerange\n";
$toUni .= "<0000> <FFFF>\n";
$toUni .= "endcodespacerange\n";
$toUni .= "1 beginbfrange\n";
$toUni .= "<0000> <FFFF> <0000>\n";
$toUni .= "endbfrange\n";
$toUni .= "endcmap\n";
$toUni .= "CMapName currentdict /CMap defineresource pop\n";
$toUni .= "end\n";
$toUni .= "end\n";
$this->writer->write('<</Length ' . strlen($toUni) . '>>');
$this->writer->stream($toUni);
$this->writer->write('endobj');
// CIDSystemInfo dictionary
$this->writer->object();
$this->writer->write('<</Registry (Adobe)');
$this->writer->write('/Ordering (UCS)');
$this->writer->write('/Supplement 0');
$this->writer->write('>>');
$this->writer->write('endobj');
// Font descriptor
$this->writer->object();
$this->writer->write('<</Type /FontDescriptor');
$this->writer->write('/FontName /' . $fontname);
foreach ($font['desc'] as $kd => $v) {
if ($asSubset && $kd === 'Flags') {
$v |= 4;
$v &= ~32;
} // SYMBOLIC font flag
$this->writer->write(' /' . $kd . ' ' . $v);
}
if ($font['panose']) {
$this->writer->write(' /Style << /Panose <' . $font['panose'] . '> >>');
}
if ($asSubset) {
$this->writer->write('/FontFile2 ' . ($this->mpdf->n + 2) . ' 0 R');
} elseif ($font['fontkey']) {
// obj ID of a stream containing a TrueType font program
$this->writer->write('/FontFile2 ' . $this->mpdf->FontFiles[$font['fontkey']]['n'] . ' 0 R');
}
$this->writer->write('>>');
$this->writer->write('endobj');
// Embed CIDToGIDMap
// A specification of the mapping from CIDs to glyph indices
if ($asSubset) {
$cidtogidmap = str_pad('', 256 * 256 * 2, "\x00");
foreach ($codeToGlyph as $cc => $glyph) {
$cidtogidmap[$cc * 2] = chr($glyph >> 8);
$cidtogidmap[$cc * 2 + 1] = chr($glyph & 0xFF);
}
$cidtogidmap = gzcompress($cidtogidmap);
} else {
// First see if there is a cached CIDToGIDMapfile
if ($this->fontCache->has($font['fontkey'] . '.cgm')) {
$cidtogidmap = $this->fontCache->load($font['fontkey'] . '.cgm');
} else {
$ttf = new TTFontFile($this->fontCache, $this->fontDescriptor);
$charToGlyph = $ttf->getCTG($font['ttffile'], $font['TTCfontID'], $this->mpdf->debugfonts, $font['useOTL']);
$cidtogidmap = str_pad('', 256 * 256 * 2, "\x00");
foreach ($charToGlyph as $cc => $glyph) {
$cidtogidmap[$cc * 2] = chr($glyph >> 8);
$cidtogidmap[$cc * 2 + 1] = chr($glyph & 0xFF);
}
unset($ttf);
$cidtogidmap = gzcompress($cidtogidmap);
$this->fontCache->binaryWrite($font['fontkey'] . '.cgm', $cidtogidmap);
}
}
$this->writer->object();
$this->writer->write('<</Length ' . strlen($cidtogidmap) . '');
$this->writer->write('/Filter /FlateDecode');
$this->writer->write('>>');
$this->writer->stream($cidtogidmap);
$this->writer->write('endobj');
// Font file
if ($asSubset) {
$this->writer->object();
$this->writer->write('<</Length ' . strlen($fontstream));
$this->writer->write('/Filter /FlateDecode');
$this->writer->write('/Length1 ' . $ttfontsize);
$this->writer->write('>>');
$this->writer->stream($fontstream);
$this->writer->write('endobj');
unset($ttf);
}
} else {
throw new \Mpdf\MpdfException(sprintf('Unsupported font type: %s (%s)', $type, $name));
}
}
}
private function writeTTFontWidths(&$font, $asSubset, $maxUni) // _putTTfontwidths
{
$character = [
'startcid' => 1,
'rangeid' => 0,
'prevcid' => -2,
'prevwidth' => -1,
'interval' => false,
'range' => [],
];
$fontCacheFilename = $font['fontkey'] . '.cw127.json';
if ($asSubset && $this->fontCache->jsonHas($fontCacheFilename)) {
$character = $this->fontCache->jsonLoad($fontCacheFilename);
$character['startcid'] = 128;
}
// for each character
$cwlen = ($asSubset) ? $maxUni + 1 : (strlen($font['cw']) / 2);
for ($cid = $character['startcid']; $cid < $cwlen; $cid++) {
if ($cid == 128 && $asSubset && (!$this->fontCache->has($fontCacheFilename))) {
$character = [
'rangeid' => $character['rangeid'],
'prevcid' => $character['prevcid'],
'prevwidth' => $character['prevwidth'],
'interval' => $character['interval'],
'range' => $character['range'],
];
$this->fontCache->jsonWrite($fontCacheFilename, $character);
}
$character1 = isset($font['cw'][$cid * 2]) ? $font['cw'][$cid * 2] : '';
$character2 = isset($font['cw'][$cid * 2 + 1]) ? $font['cw'][$cid * 2 + 1] : '';
if ($character1 === "\00" && $character2 === "\00") {
continue;
}
$width = (ord($character1) << 8) + ord($character2);
if ($width === 65535) {
$width = 0;
}
if ($asSubset && $cid > 255 && (!isset($font['subset'][$cid]) || !$font['subset'][$cid])) {
continue;
}
if ($asSubset && $cid > 0xFFFF) {
continue;
} // mPDF 6
if (!isset($font['dw']) || (isset($font['dw']) && $width != $font['dw'])) {
if ($cid === ($character['prevcid'] + 1)) {
// consecutive CID
if ($width === $character['prevwidth']) {
if (isset($character['range'][$character['rangeid']][0]) && $width === $character['range'][$character['rangeid']][0]) {
$character['range'][$character['rangeid']][] = $width;
} else {
array_pop($character['range'][$character['rangeid']]);
// new range
$character['rangeid'] = $character['prevcid'];
$character['range'][$character['rangeid']] = [];
$character['range'][$character['rangeid']][] = $character['prevwidth'];
$character['range'][$character['rangeid']][] = $width;
}
$character['interval'] = true;
$character['range'][$character['rangeid']]['interval'] = true;
} else {
if ($character['interval']) {
// new range
$character['rangeid'] = $cid;
$character['range'][$character['rangeid']] = [];
$character['range'][$character['rangeid']][] = $width;
} else {
$character['range'][$character['rangeid']][] = $width;
}
$character['interval'] = false;
}
} else {
// new range
$character['rangeid'] = $cid;
$character['range'][$character['rangeid']] = [];
$character['range'][$character['rangeid']][] = $width;
$character['interval'] = false;
}
$character['prevcid'] = $cid;
$character['prevwidth'] = $width;
}
}
$w = $this->writeFontRanges($character['range']);
$this->writer->write($w);
if (!$asSubset) {
$this->fontCache->binaryWrite($font['fontkey'] . '.cw', $w);
}
}
private function writeFontRanges(&$range) // _putfontranges
{
// optimize ranges
$prevk = -1;
$nextk = -1;
$prevint = false;
foreach ($range as $k => $ws) {
$cws = count($ws);
if (($k == $nextk) and ( !$prevint) and ( (!isset($ws['interval'])) or ( $cws < 4))) {
if (isset($range[$k]['interval'])) {
unset($range[$k]['interval']);
}
$range[$prevk] = array_merge($range[$prevk], $range[$k]);
unset($range[$k]);
} else {
$prevk = $k;
}
$nextk = $k + $cws;
if (isset($ws['interval'])) {
if ($cws > 3) {
$prevint = true;
} else {
$prevint = false;
}
unset($range[$k]['interval']);
--$nextk;
} else {
$prevint = false;
}
}
// output data
$w = '';
foreach ($range as $k => $ws) {
if (count(array_count_values($ws)) === 1) {
// interval mode is more compact
$w .= ' ' . $k . ' ' . ($k + count($ws) - 1) . ' ' . $ws[0];
} else {
// range mode
$w .= ' ' . $k . ' [ ' . implode(' ', $ws) . ' ]' . "\n";
}
}
return '/W [' . $w . ' ]';
}
private function writeFontWidths(&$font, $cidoffset = 0) // _putfontwidths
{
ksort($font['cw']);
unset($font['cw'][65535]);
$rangeid = 0;
$range = [];
$prevcid = -2;
$prevwidth = -1;
$interval = false;
// for each character
foreach ($font['cw'] as $cid => $width) {
$cid -= $cidoffset;
if (!isset($font['dw']) || (isset($font['dw']) && $width != $font['dw'])) {
if ($cid === ($prevcid + 1)) {
// consecutive CID
if ($width === $prevwidth) {
if ($width === $range[$rangeid][0]) {
$range[$rangeid][] = $width;
} else {
array_pop($range[$rangeid]);
// new range
$rangeid = $prevcid;
$range[$rangeid] = [];
$range[$rangeid][] = $prevwidth;
$range[$rangeid][] = $width;
}
$interval = true;
$range[$rangeid]['interval'] = true;
} else {
if ($interval) {
// new range
$rangeid = $cid;
$range[$rangeid] = [];
$range[$rangeid][] = $width;
} else {
$range[$rangeid][] = $width;
}
$interval = false;
}
} else {
// new range
$rangeid = $cid;
$range[$rangeid] = [];
$range[$rangeid][] = $width;
$interval = false;
}
$prevcid = $cid;
$prevwidth = $width;
}
}
$this->writer->write($this->writeFontRanges($range));
}
// from class PDF_Chinese CJK EXTENSIONS
public function writeType0(&$font) // _putType0
{
// Type0
$this->writer->write('/Subtype /Type0');
$this->writer->write('/BaseFont /' . $font['name'] . '-' . $font['CMap']);
$this->writer->write('/Encoding /' . $font['CMap']);
$this->writer->write('/DescendantFonts [' . ($this->mpdf->n + 1) . ' 0 R]');
$this->writer->write('>>');
$this->writer->write('endobj');
// CIDFont
$this->writer->object();
$this->writer->write('<</Type /Font');
$this->writer->write('/Subtype /CIDFontType0');
$this->writer->write('/BaseFont /' . $font['name']);
$cidinfo = '/Registry ' . $this->writer->string('Adobe');
$cidinfo .= ' /Ordering ' . $this->writer->string($font['registry']['ordering']);
$cidinfo .= ' /Supplement ' . $font['registry']['supplement'];
$this->writer->write('/CIDSystemInfo <<' . $cidinfo . '>>');
$this->writer->write('/FontDescriptor ' . ($this->mpdf->n + 1) . ' 0 R');
if (isset($font['MissingWidth'])) {
$this->writer->write('/DW ' . $font['MissingWidth'] . '');
}
$this->writeFontWidths($font, 31);
$this->writer->write('>>');
$this->writer->write('endobj');
// Font descriptor
$this->writer->object();
$s = '<</Type /FontDescriptor /FontName /' . $font['name'];
foreach ($font['desc'] as $k => $v) {
if ($k !== 'Style') {
$s .= ' /' . $k . ' ' . $v . '';
}
}
$this->writer->write($s . '>>');
$this->writer->write('endobj');
}
}
|