Initial commit

This commit is contained in:
2026-08-20 11:41:17 +00:00
commit 6949695eac
2334 changed files with 646393 additions and 0 deletions
+622
View File
@@ -0,0 +1,622 @@
<?php
declare(strict_types=1);
/**
* Core.php
*
* @since 2011-05-23
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-font
*
* This file is part of tc-lib-pdf-font software library.
*/
namespace Com\Tecnick\Pdf\Font\Import;
use Com\Tecnick\File\File as ObjFile;
use Com\Tecnick\Pdf\Font\Exception as FontException;
use Com\Tecnick\Unicode\Data\Encoding;
/**
* Com\Tecnick\Pdf\Font\Import\Core
*
* @since 2011-05-23
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-font
*
* @phpstan-import-type TFontData from \Com\Tecnick\Pdf\Font\Load
* @SuppressWarnings("PHPMD.ExcessiveClassComplexity")
*/
class Core
{
/**
* Adobe Glyph List subset covering all Core14 AFM glyph names.
* Maps AFM glyph names to Unicode codepoints.
*
* @var array<string, int>
*/
private const GLYPH_UNICODE = [
'A' => 0x0041,
'AE' => 0x00C6,
'Aacute' => 0x00C1,
'Abreve' => 0x0102,
'Acircumflex' => 0x00C2,
'Adieresis' => 0x00C4,
'Agrave' => 0x00C0,
'Amacron' => 0x0100,
'Aogonek' => 0x0104,
'Aring' => 0x00C5,
'Atilde' => 0x00C3,
'B' => 0x0042,
'C' => 0x0043,
'Cacute' => 0x0106,
'Ccaron' => 0x010C,
'Ccedilla' => 0x00C7,
'D' => 0x0044,
'Dcaron' => 0x010E,
'Dcroat' => 0x0110,
'Delta' => 0x2206,
'E' => 0x0045,
'Eacute' => 0x00C9,
'Ecaron' => 0x011A,
'Ecircumflex' => 0x00CA,
'Edieresis' => 0x00CB,
'Edotaccent' => 0x0116,
'Egrave' => 0x00C8,
'Emacron' => 0x0112,
'Eogonek' => 0x0118,
'Eth' => 0x00D0,
'Euro' => 0x20AC,
'F' => 0x0046,
'G' => 0x0047,
'Gbreve' => 0x011E,
'Gcommaaccent' => 0x0122,
'H' => 0x0048,
'I' => 0x0049,
'Iacute' => 0x00CD,
'Icircumflex' => 0x00CE,
'Idieresis' => 0x00CF,
'Idotaccent' => 0x0130,
'Igrave' => 0x00CC,
'Imacron' => 0x012A,
'Iogonek' => 0x012E,
'J' => 0x004A,
'K' => 0x004B,
'Kcommaaccent' => 0x0136,
'L' => 0x004C,
'Lacute' => 0x0139,
'Lcaron' => 0x013D,
'Lcommaaccent' => 0x013B,
'Lslash' => 0x0141,
'M' => 0x004D,
'N' => 0x004E,
'Nacute' => 0x0143,
'Ncaron' => 0x0147,
'Ncommaaccent' => 0x0145,
'Ntilde' => 0x00D1,
'O' => 0x004F,
'OE' => 0x0152,
'Oacute' => 0x00D3,
'Ocircumflex' => 0x00D4,
'Odieresis' => 0x00D6,
'Ograve' => 0x00D2,
'Ohungarumlaut' => 0x0150,
'Omacron' => 0x014C,
'Oslash' => 0x00D8,
'Otilde' => 0x00D5,
'P' => 0x0050,
'Q' => 0x0051,
'R' => 0x0052,
'Racute' => 0x0154,
'Rcaron' => 0x0158,
'Rcommaaccent' => 0x0156,
'S' => 0x0053,
'Sacute' => 0x015A,
'Scaron' => 0x0160,
'Scedilla' => 0x015E,
'Scommaaccent' => 0x0218,
'T' => 0x0054,
'Tcaron' => 0x0164,
'Tcommaaccent' => 0x0162,
'Thorn' => 0x00DE,
'U' => 0x0055,
'Uacute' => 0x00DA,
'Ucircumflex' => 0x00DB,
'Udieresis' => 0x00DC,
'Ugrave' => 0x00D9,
'Uhungarumlaut' => 0x0170,
'Umacron' => 0x016A,
'Uogonek' => 0x0172,
'Uring' => 0x016E,
'V' => 0x0056,
'W' => 0x0057,
'X' => 0x0058,
'Y' => 0x0059,
'Yacute' => 0x00DD,
'Ydieresis' => 0x0178,
'Z' => 0x005A,
'Zacute' => 0x0179,
'Zcaron' => 0x017D,
'Zdotaccent' => 0x017B,
'a' => 0x0061,
'aacute' => 0x00E1,
'abreve' => 0x0103,
'acircumflex' => 0x00E2,
'acute' => 0x00B4,
'adieresis' => 0x00E4,
'ae' => 0x00E6,
'agrave' => 0x00E0,
'amacron' => 0x0101,
'ampersand' => 0x0026,
'aogonek' => 0x0105,
'aring' => 0x00E5,
'asciicircum' => 0x005E,
'asciitilde' => 0x007E,
'asterisk' => 0x002A,
'at' => 0x0040,
'atilde' => 0x00E3,
'b' => 0x0062,
'backslash' => 0x005C,
'bar' => 0x007C,
'braceleft' => 0x007B,
'braceright' => 0x007D,
'bracketleft' => 0x005B,
'bracketright' => 0x005D,
'breve' => 0x02D8,
'brokenbar' => 0x00A6,
'bullet' => 0x2022,
'c' => 0x0063,
'cacute' => 0x0107,
'caron' => 0x02C7,
'ccaron' => 0x010D,
'ccedilla' => 0x00E7,
'cedilla' => 0x00B8,
'cent' => 0x00A2,
'circumflex' => 0x02C6,
'colon' => 0x003A,
'comma' => 0x002C,
'commaaccent' => 0x0326,
'copyright' => 0x00A9,
'currency' => 0x00A4,
'd' => 0x0064,
'dagger' => 0x2020,
'daggerdbl' => 0x2021,
'dcaron' => 0x010F,
'dcroat' => 0x0111,
'degree' => 0x00B0,
'dieresis' => 0x00A8,
'divide' => 0x00F7,
'dollar' => 0x0024,
'dotaccent' => 0x02D9,
'dotlessi' => 0x0131,
'e' => 0x0065,
'eacute' => 0x00E9,
'ecaron' => 0x011B,
'ecircumflex' => 0x00EA,
'edieresis' => 0x00EB,
'edotaccent' => 0x0117,
'egrave' => 0x00E8,
'eight' => 0x0038,
'ellipsis' => 0x2026,
'emacron' => 0x0113,
'emdash' => 0x2014,
'endash' => 0x2013,
'eogonek' => 0x0119,
'equal' => 0x003D,
'eth' => 0x00F0,
'exclam' => 0x0021,
'exclamdown' => 0x00A1,
'f' => 0x0066,
'fi' => 0xFB01,
'five' => 0x0035,
'fl' => 0xFB02,
'florin' => 0x0192,
'four' => 0x0034,
'fraction' => 0x2044,
'g' => 0x0067,
'gbreve' => 0x011F,
'gcommaaccent' => 0x0123,
'germandbls' => 0x00DF,
'grave' => 0x0060,
'greater' => 0x003E,
'greaterequal' => 0x2265,
'guillemotleft' => 0x00AB,
'guillemotright' => 0x00BB,
'guilsinglleft' => 0x2039,
'guilsinglright' => 0x203A,
'h' => 0x0068,
'hungarumlaut' => 0x02DD,
'hyphen' => 0x002D,
'i' => 0x0069,
'iacute' => 0x00ED,
'icircumflex' => 0x00EE,
'idieresis' => 0x00EF,
'igrave' => 0x00EC,
'imacron' => 0x012B,
'iogonek' => 0x012F,
'j' => 0x006A,
'k' => 0x006B,
'kcommaaccent' => 0x0137,
'l' => 0x006C,
'lacute' => 0x013A,
'lcaron' => 0x013E,
'lcommaaccent' => 0x013C,
'less' => 0x003C,
'lessequal' => 0x2264,
'logicalnot' => 0x00AC,
'lozenge' => 0x25CA,
'lslash' => 0x0142,
'm' => 0x006D,
'macron' => 0x00AF,
'minus' => 0x2212,
'mu' => 0x00B5,
'multiply' => 0x00D7,
'n' => 0x006E,
'nacute' => 0x0144,
'ncaron' => 0x0148,
'ncommaaccent' => 0x0146,
'nine' => 0x0039,
'notequal' => 0x2260,
'ntilde' => 0x00F1,
'numbersign' => 0x0023,
'o' => 0x006F,
'oacute' => 0x00F3,
'ocircumflex' => 0x00F4,
'odieresis' => 0x00F6,
'oe' => 0x0153,
'ogonek' => 0x02DB,
'ograve' => 0x00F2,
'ohungarumlaut' => 0x0151,
'omacron' => 0x014D,
'one' => 0x0031,
'onehalf' => 0x00BD,
'onequarter' => 0x00BC,
'onesuperior' => 0x00B9,
'ordfeminine' => 0x00AA,
'ordmasculine' => 0x00BA,
'oslash' => 0x00F8,
'otilde' => 0x00F5,
'p' => 0x0070,
'paragraph' => 0x00B6,
'parenleft' => 0x0028,
'parenright' => 0x0029,
'partialdiff' => 0x2202,
'percent' => 0x0025,
'period' => 0x002E,
'periodcentered' => 0x00B7,
'perthousand' => 0x2030,
'plus' => 0x002B,
'plusminus' => 0x00B1,
'q' => 0x0071,
'question' => 0x003F,
'questiondown' => 0x00BF,
'quotedbl' => 0x0022,
'quotedblbase' => 0x201E,
'quotedblleft' => 0x201C,
'quotedblright' => 0x201D,
'quoteleft' => 0x2018,
'quoteright' => 0x2019,
'quotesinglbase' => 0x201A,
'quotesingle' => 0x0027,
'r' => 0x0072,
'racute' => 0x0155,
'radical' => 0x221A,
'rcaron' => 0x0159,
'rcommaaccent' => 0x0157,
'registered' => 0x00AE,
'ring' => 0x02DA,
's' => 0x0073,
'sacute' => 0x015B,
'scaron' => 0x0161,
'scedilla' => 0x015F,
'scommaaccent' => 0x0219,
'section' => 0x00A7,
'semicolon' => 0x003B,
'seven' => 0x0037,
'six' => 0x0036,
'slash' => 0x002F,
'space' => 0x0020,
'sterling' => 0x00A3,
'summation' => 0x2211,
't' => 0x0074,
'tcaron' => 0x0165,
'tcommaaccent' => 0x0163,
'thorn' => 0x00FE,
'three' => 0x0033,
'threequarters' => 0x00BE,
'threesuperior' => 0x00B3,
'tilde' => 0x02DC,
'trademark' => 0x2122,
'two' => 0x0032,
'twosuperior' => 0x00B2,
'u' => 0x0075,
'uacute' => 0x00FA,
'ucircumflex' => 0x00FB,
'udieresis' => 0x00FC,
'ugrave' => 0x00F9,
'uhungarumlaut' => 0x0171,
'umacron' => 0x016B,
'underscore' => 0x005F,
'uogonek' => 0x0173,
'uring' => 0x016F,
'v' => 0x0076,
'w' => 0x0077,
'x' => 0x0078,
'y' => 0x0079,
'yacute' => 0x00FD,
'ydieresis' => 0x00FF,
'yen' => 0x00A5,
'z' => 0x007A,
'zacute' => 0x017A,
'zcaron' => 0x017E,
'zdotaccent' => 0x017C,
'zero' => 0x0030,
];
/**
* WinAnsi (cp1252) glyph-name → byte index (inverse of ENCMAP['cp1252']).
* Built once on first use.
*
* @var array<string, int>|null
*/
private static ?array $winAnsiByName = null;
/**
* Unicode-keyed widths accumulated during AFM parsing.
*
* @var array<int, int>
*/
private array $cwu = [];
/**
* File helper used to load font definition files.
*/
protected ObjFile $fileHelper;
/**
* @param string $font Content of the input font file
* @param TFontData $fdt Extracted font metrics
* @param ObjFile $fileHelper File helper for font loading.
*
* @throws FontException in case of error
*/
public function __construct(
protected string $font,
protected array $fdt,
ObjFile $fileHelper,
) {
$this->fileHelper = $fileHelper;
$this->process();
}
/**
* Get all the extracted font metrics
*
* @return TFontData
*/
public function getFontMetrics(): array
{
return $this->fdt;
}
protected function setFlags(): void
{
if ($this->fdt['FontName'] === 'Symbol' || $this->fdt['FontName'] === 'ZapfDingbats') {
$this->fdt['Flags'] |= 4;
} else {
$this->fdt['Flags'] |= 32;
}
if ($this->fdt['IsFixedPitch']) {
$this->fdt['Flags'] = (int) $this->fdt['Flags'] | 1;
}
if ((int) $this->fdt['ItalicAngle'] !== 0) {
$this->fdt['Flags'] = (int) $this->fdt['Flags'] | 64;
}
}
/**
* Set Char widths
*
* @param array<int, int> $cwidths Extracted widths
*/
protected function setCharWidths(array $cwidths): void
{
$this->fdt['MissingWidth'] = 600;
if (isset($cwidths[32]) && $cwidths[32] !== 0) {
$this->fdt['MissingWidth'] = $cwidths[32];
}
$this->fdt['MaxWidth'] = (int) $this->fdt['MissingWidth'];
$this->fdt['AvgWidth'] = 0;
$this->fdt['cw'] = [];
for ($cid = 0; $cid <= 255; ++$cid) {
if (isset($cwidths[$cid])) {
if ($cwidths[$cid] > $this->fdt['MaxWidth']) {
$this->fdt['MaxWidth'] = $cwidths[$cid];
}
$this->fdt['AvgWidth'] += $cwidths[$cid];
$this->fdt['cw'][$cid] = $cwidths[$cid];
} else {
$this->fdt['cw'][$cid] = (int) $this->fdt['MissingWidth'];
}
}
$numWidths = \count($cwidths);
$this->fdt['AvgWidth'] = $numWidths > 0 ? (int) \round($this->fdt['AvgWidth'] / $numWidths) : 0;
}
/**
* Build (once) the WinAnsi glyph-name → byte-index reverse map.
*
* @return array<string, int>
*/
private static function getWinAnsiByName(): array
{
if (self::$winAnsiByName === null) {
self::$winAnsiByName = [];
// Iterate high-to-low so the lowest byte wins for duplicate names
// (e.g. 'space' appears at bytes 32 and 160 — we want 32).
for ($cid = 255; $cid >= 0; --$cid) {
$name = Encoding::MAP['cp1252'][$cid];
if ($name !== '.notdef') {
self::$winAnsiByName[$name] = $cid;
}
}
}
return self::$winAnsiByName;
}
/**
* Extract Metrics
*/
protected function extractMetrics(): void
{
$cwd = [];
$this->cwu = [];
$this->fdt['cbbox'] = [];
$lines = \explode("\n", \str_replace("\r", '', $this->font));
// process each row
foreach ($lines as $line) {
$col = \explode(' ', \rtrim($line));
if (\count($col) > 1) {
$this->processMetricRow($col, $cwd);
}
}
$this->fdt['Leading'] = 0;
$this->fdt['cwu'] = $this->cwu;
$this->setCharWidths($cwd);
}
/**
* Extract Metrics
*
* @param array<int, string> $col Array containing row elements to process
* @param array<int, int> $cwd Array containing cid widths
*
* @SuppressWarnings("PHPMD.CyclomaticComplexity")
*/
protected function processMetricRow(array $col, array &$cwd): void
{
switch ($col[0]) {
case 'IsFixedPitch':
$this->fdt['IsFixedPitch'] = $col[1] === 'true';
break;
case 'FontBBox':
$this->fdt['FontBBox'] = [(int) $col[1], (int) $col[2], (int) $col[3], (int) $col[4]];
break;
case 'C':
$name = $col[7] ?? '';
if ($name === '' || $name === '.notdef') {
break;
}
$width = (int) $col[4];
// Map glyph name to the WinAnsi byte actually used in the PDF stream.
$winAnsi = self::getWinAnsiByName();
$winansiCid = $winAnsi[$name] ?? null;
if ($winansiCid !== null) {
$cwd[$winansiCid] = $width;
if (isset($col[14]) && $col[14] !== '') {
$this->fdt['cbbox'][$winansiCid] = [
(int) $col[10],
(int) $col[11],
(int) $col[12],
(int) $col[13],
];
}
}
// Also store under the Unicode codepoint for Stack::getCharWidth().
$unicode = self::GLYPH_UNICODE[$name] ?? null;
if ($unicode !== null) {
$this->cwu[$unicode] = $width;
}
break;
case 'FontName':
case 'FullName':
case 'FamilyName':
case 'Weight':
case 'CharacterSet':
case 'Version':
case 'EncodingScheme':
$this->fdt[$col[0]] = $col[1];
break;
case 'ItalicAngle':
case 'UnderlinePosition':
case 'UnderlineThickness':
case 'CapHeight':
case 'XHeight':
case 'Ascender':
case 'Descender':
case 'StdHW':
case 'StdVW':
$this->fdt[$col[0]] = (int) $col[1];
break;
}
}
/**
* Map values to the correct key name
*
* @throws FontException
*/
protected function remapValues(): void
{
// rename properties
$this->fdt['name'] = $this->fdt['FullName'];
$this->fdt['underlinePosition'] = $this->fdt['UnderlinePosition'];
$this->fdt['underlineThickness'] = $this->fdt['UnderlineThickness'];
$this->fdt['italicAngle'] = $this->fdt['ItalicAngle'];
$this->fdt['Ascent'] = $this->fdt['Ascender'];
$this->fdt['Descent'] = $this->fdt['Descender'];
$this->fdt['StemV'] = $this->fdt['StdVW'];
$this->fdt['StemH'] = $this->fdt['StdHW'];
$name = \preg_replace('/[^a-zA-Z0-9_\-]/', '', $this->fdt['name']);
if ($name === null) {
throw new FontException('Invalid font name');
}
$this->fdt['name'] = $name;
$this->fdt['bbox'] = \implode(' ', $this->fdt['FontBBox']);
}
protected function setMissingValues(): void
{
$this->fdt['Descender'] = $this->fdt['FontBBox'][1];
$this->fdt['Ascender'] = $this->fdt['FontBBox'][3];
if ($this->fdt['CapHeight'] === 0) {
$this->fdt['CapHeight'] = $this->fdt['Ascender'];
}
}
/**
* Process Core font
*
* @throws FontException
*/
protected function process(): void
{
$this->extractMetrics();
$this->setFlags();
$this->setMissingValues();
$this->remapValues();
}
}
File diff suppressed because it is too large Load Diff
+419
View File
@@ -0,0 +1,419 @@
<?php
declare(strict_types=1);
/**
* TypeOne.php
*
* @since 2011-05-23
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-font
*
* This file is part of tc-lib-pdf-font software library.
*/
namespace Com\Tecnick\Pdf\Font\Import;
use Com\Tecnick\File\Exception as FileException;
use Com\Tecnick\Pdf\Font\Exception as FontException;
use Com\Tecnick\Unicode\Data\Encoding;
/**
* Com\Tecnick\Pdf\Font\Import\TypeOne
*
* @since 2011-05-23
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-font
*
* @SuppressWarnings("PHPMD.ExcessiveClassComplexity")
*/
class TypeOne extends \Com\Tecnick\Pdf\Font\Import\Core
{
/**
* Store font data
*
* @throws FileException
* @throws FontException
*/
protected function storeFontData(): void
{
// read first segment
$dat = \unpack('Cmarker/Ctype/Vsize', \substr($this->font, 0, 6));
if ($dat === false || $dat['marker'] !== 128) {
throw new FontException('Font file is not a valid binary Type1');
}
$this->fdt['size1'] = $dat['size'];
$fontlen = \strlen($this->font);
// the first segment plus the 6-byte header of the second segment must fit in the file
if ((6 + $this->fdt['size1'] + 6) > $fontlen) {
throw new FontException('Type1 font segment 1 length exceeds the file size');
}
$data = \substr($this->font, 6, $this->fdt['size1']);
// read second segment
$dat = \unpack('Cmarker/Ctype/Vsize', \substr($this->font, 6 + $this->fdt['size1'], 6));
if ($dat === false || $dat['marker'] !== 128) {
throw new FontException('Font file is not a valid binary Type1');
}
$this->fdt['size2'] = $dat['size'];
if ((12 + $this->fdt['size1'] + $this->fdt['size2']) > $fontlen) {
throw new FontException('Type1 font segment 2 length exceeds the file size');
}
$this->fdt['encrypted'] = \substr($this->font, 12 + $this->fdt['size1'], $this->fdt['size2']);
$data .= $this->fdt['encrypted'];
// store compressed font
$this->fdt['file'] = $this->fdt['file_name'] . '.z';
$fpt = $this->fileHelper->fopenLocal($this->fdt['dir'] . $this->fdt['file'], 'wb');
$cmpr = \gzcompress($data);
if ($cmpr === false) {
throw new FontException('Unable to compress font data');
}
\fwrite($fpt, $cmpr);
\fclose($fpt);
}
/**
* Extract Font information
*
* @throws FontException
*/
protected function extractFontInfo(): void
{
$matches = [];
if (
\preg_match('#/FontName[\s]*+\/([^\s]*+)#', $this->font, $matches) !== 1
&& \preg_match('#/FullName[\s]*+\(([^\)]*+)#', $this->font, $matches) !== 1
) {
throw new FontException('Unable to extract font name');
}
$name = \preg_replace('/[^a-zA-Z0-9_\-]/', '', $matches[1]);
if ($name === null) {
throw new FontException('Unable to extract font name');
}
$this->fdt['name'] = $name;
$bvl = [0, 0, 0, 0];
if (\preg_match('#/FontBBox[\s]*+{([^}]*+)#', $this->font, $matches) === 1) {
$rawbvl = \explode(' ', \trim($matches[1]));
$bvl = [
(int) $rawbvl[0],
(int) ($rawbvl[1] ?? 0),
(int) ($rawbvl[2] ?? 0),
(int) ($rawbvl[3] ?? 0),
];
}
$this->fdt['bbox'] = \implode(' ', $bvl);
$this->fdt['Ascent'] = $bvl[3];
$this->fdt['Descent'] = $bvl[1];
$this->fdt['italicAngle'] = \preg_match('#/ItalicAngle[\s]*+([0-9\+\-]*+)#', $this->font, $matches) === 1
? (int) $matches[1]
: 0;
if ($this->fdt['italicAngle'] !== 0) {
$this->fdt['Flags'] |= 64;
}
$this->fdt['underlinePosition'] = \preg_match('#/UnderlinePosition[\s]*+([0-9\+\-]*+)#', $this->font, $matches)
=== 1
? (int) $matches[1]
: 0;
$this->fdt['underlineThickness'] = \preg_match(
'#/UnderlineThickness[\s]*+([0-9\+\-]*+)#',
$this->font,
$matches,
) === 1
? (int) $matches[1]
: 0;
if (\preg_match('#/isFixedPitch[\s]*+([^\s]*+)#', $this->font, $matches) === 1 && $matches[1] === 'true') {
$this->fdt['Flags'] = (int) $this->fdt['Flags'] | 1;
}
$this->fdt['weight'] = 'Book';
if (\preg_match('#/Weight[\s]*+\(([^\)]*+)#', $this->font, $matches) === 1 && $matches[1] !== '') {
$this->fdt['weight'] = \strtolower($matches[1]);
}
$this->fdt['Leading'] = 0;
}
/**
* Extract Font information
*
* @return array<string, int>
*/
protected function getInternalMap(): array
{
$imap = [];
$fmap = [];
$matches = \preg_match_all('#dup[\s]([0-9]+)[\s]*+/([^\s]*+)[\s]put#sU', $this->font, $fmap, PREG_SET_ORDER);
if ($matches !== false && $matches >= 1) {
foreach ($fmap as $val) {
$imap[$val[2]] = (int) $val[1];
}
}
return $imap;
}
/**
* Decrypt eexec encrypted part
*/
protected function getEplain(): string
{
$csr = 55_665; // eexec encryption constant
$cc1 = 52_845;
$cc2 = 22_719;
$elen = \strlen($this->fdt['encrypted']);
$eplain = '';
for ($idx = 0; $idx < $elen; ++$idx) {
$chr = \ord($this->fdt['encrypted'][$idx]);
$eplain .= \chr($chr ^ ($csr >> 8));
$csr = ((($chr + $csr) * $cc1) + $cc2) % 65_536;
}
return $eplain;
}
/**
* Extract eexec info
*
* @return array<int, array<int, string>>
*/
protected function extractEplainInfo(): array
{
$eplain = $this->getEplain();
$matches = [];
if (\preg_match('#/ForceBold[\s]*+([^\s]*+)#', $eplain, $matches) === 1 && $matches[1] === 'true') {
$this->fdt['Flags'] |= 0x4_0000;
}
$this->extractStem($eplain);
if (\preg_match('#/BlueValues[\s]*+\[([^\]]*+)#', $eplain, $matches) === 1) {
$bvl = \explode(' ', $matches[1]);
if (\count($bvl) >= 6) {
$vl1 = (int) $bvl[2];
$vl2 = (int) $bvl[4];
$this->fdt['XHeight'] = \min($vl1, $vl2);
$this->fdt['CapHeight'] = \max($vl1, $vl2);
}
}
$this->getRandomBytes($eplain);
return $this->getCharstringData($eplain);
}
/**
* Extract eexec info
*
* @param string $eplain Decoded eexec encrypted part
*/
protected function extractStem(string $eplain): void
{
$matches = [];
if (\preg_match('#/StdVW[\s]*+\[([^\]]*+)#', $eplain, $matches) === 1) {
$this->fdt['StemV'] = (int) $matches[1];
} elseif ($this->fdt['weight'] === 'bold' || $this->fdt['weight'] === 'black') {
$this->fdt['StemV'] = 123;
} else {
$this->fdt['StemV'] = 70;
}
$this->fdt['StemH'] = \preg_match('#/StdHW[\s]*+\[([^\]]*+)#', $eplain, $matches) === 1
? (int) $matches[1]
: 30;
if (\preg_match('#/Cap[X]?Height[\s]*+\[([^\]]*+)#', $eplain, $matches) === 1) {
$this->fdt['CapHeight'] = (int) $matches[1];
} else {
$this->fdt['CapHeight'] = (int) $this->fdt['Ascent'];
}
$this->fdt['XHeight'] = (int) $this->fdt['Ascent'] + (int) $this->fdt['Descent'];
}
/**
* Get the number of random bytes at the beginning of charstrings
*/
protected function getRandomBytes(string $eplain): void
{
$this->fdt['lenIV'] = 4;
$matches = [];
if (\preg_match('#/lenIV[\s]*+([\d]*+)#', $eplain, $matches) === 1) {
$this->fdt['lenIV'] = (int) $matches[1];
}
}
/**
* @return array<int, array<int, string>>
*/
protected function getCharstringData(string $eplain): array
{
$this->fdt['enc_map'] = [];
$charstringsPos = \strpos($eplain, '/CharStrings');
if ($charstringsPos === false) {
return [];
}
$eplain = \substr($eplain, $charstringsPos + 1);
$matches = [];
\preg_match_all('#/([A-Za-z0-9\.]*+)[\s][0-9]+[\s]RD[\s](.*)[\s]ND#sU', $eplain, $matches, PREG_SET_ORDER);
/** @var array<int, array<int, string>> $matches */
if ($this->fdt['enc'] === '') {
return $matches;
}
if (!isset(Encoding::MAP[$this->fdt['enc']])) {
return $matches;
}
$this->fdt['enc_map'] = Encoding::MAP[$this->fdt['enc']];
return $matches;
}
/**
* get CID
*
* @param array<string, int> $imap
* @param array<int, string> $val
*/
protected function getCid(array $imap, array $val): int
{
if (isset($imap[$val[1]])) {
return $imap[$val[1]];
}
$cid = \array_search($val[1], $this->fdt['enc_map'], true);
if ($cid === false) {
return 0;
}
if ($cid > 1000) {
return 1000;
}
return (int) $cid;
}
/**
* Decode number
*
* @param array<int, int> $ccom
* @param array<int, int> $cdec
* @param array<int, int> $cwidths
*
* @throws FontException
*/
protected function decodeNumber(int $idx, int &$cck, int &$cid, array &$ccom, array &$cdec, array &$cwidths): int
{
if ($ccom[$idx] === 255) {
if (!isset($ccom[$idx + 4])) {
throw new FontException('Truncated Type1 charstring number operand');
}
$sval = \chr($ccom[$idx + 1]) . \chr($ccom[$idx + 2]) . \chr($ccom[$idx + 3]) . \chr($ccom[$idx + 4]);
$vsval = \unpack('li', $sval);
if ($vsval === false) {
throw new FontException('Unable to unpack number');
}
$cdec[$cck] = (int) $vsval['i'];
return $idx + 5;
}
if ($ccom[$idx] >= 251) {
if (!isset($ccom[$idx + 1])) {
throw new FontException('Truncated Type1 charstring number operand');
}
$cdec[$cck] = (-($ccom[$idx] - 251) * 256) - $ccom[$idx + 1] - 108;
return $idx + 2;
}
if ($ccom[$idx] >= 247) {
if (!isset($ccom[$idx + 1])) {
throw new FontException('Truncated Type1 charstring number operand');
}
$cdec[$cck] = (($ccom[$idx] - 247) * 256) + $ccom[$idx + 1] + 108;
return $idx + 2;
}
if ($ccom[$idx] >= 32) {
$cdec[$cck] = $ccom[$idx] - 139;
return ++$idx;
}
$cdec[$cck] = $ccom[$idx];
if ($cck <= 0) {
return ++$idx;
}
if ($cdec[$cck] !== 13) {
return ++$idx;
}
// hsbw command: update width
$cwidths[$cid] = $cdec[$cck - 1];
return ++$idx;
}
/**
* Process Type1 font
*
* @throws FileException
* @throws FontException
*/
protected function process(): void
{
$this->storeFontData();
$this->extractFontInfo();
$imap = $this->getInternalMap();
$matches = $this->extractEplainInfo();
$cwidths = [];
$cc1 = 52_845;
$cc2 = 22_719;
foreach ($matches as $match) {
$cid = $this->getCid($imap, $match);
// decrypt charstring encrypted part
$csr = 4330; // charstring encryption constant
$ccd = $match[2];
$clen = \strlen($ccd);
$ccom = [];
for ($idx = 0; $idx < $clen; ++$idx) {
$chr = \ord($ccd[$idx]);
$ccom[] = $chr ^ ($csr >> 8);
$csr = ((($chr + $csr) * $cc1) + $cc2) % 65_536;
}
// decode numbers
$cdec = [];
$cck = 0;
$idx = $this->fdt['lenIV'];
while ($idx < $clen) {
$idx = $this->decodeNumber($idx, $cck, $cid, $ccom, $cdec, $cwidths);
++$cck;
}
}
$this->setCharWidths($cwidths);
}
}