generated from jric11/baseProject
Initial commit
This commit is contained in:
+342
@@ -0,0 +1,342 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Buffer.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;
|
||||
|
||||
use Com\Tecnick\File\File as ObjFile;
|
||||
use Com\Tecnick\Pdf\Font\Exception as FontException;
|
||||
|
||||
/**
|
||||
* Com\Tecnick\Pdf\Font\Buffer
|
||||
*
|
||||
* @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-type TFileOptions array{
|
||||
* allowedHosts?: array<string>,
|
||||
* maxRemoteSize?: int,
|
||||
* curlopts?: array<int, bool|int|string>,
|
||||
* defaultCurlOpts?: array<int, bool|int|string>,
|
||||
* fixedCurlOpts?: array<int, bool|int|string>
|
||||
* }
|
||||
*
|
||||
* @phpstan-import-type TFontData from Load
|
||||
*/
|
||||
abstract class Buffer
|
||||
{
|
||||
/**
|
||||
* Array containing all fonts data
|
||||
*
|
||||
* @var array<string, TFontData>
|
||||
*/
|
||||
protected array $font = [];
|
||||
|
||||
/**
|
||||
* Font counter
|
||||
*/
|
||||
protected int $numfonts = 0;
|
||||
|
||||
/**
|
||||
* Cache mapping a (font family, style) pair to its resolved font key, so repeated
|
||||
* lookups of an already-loaded font skip constructing a throwaway Font object.
|
||||
*
|
||||
* @var array<string, array<string, string>>
|
||||
*/
|
||||
protected array $fontKeyCache = [];
|
||||
|
||||
/**
|
||||
* Array containing encoding differences
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected array $encdiff = [];
|
||||
|
||||
/**
|
||||
* Index for Encoding differences
|
||||
*/
|
||||
protected int $numdiffs = 0;
|
||||
|
||||
/**
|
||||
* Array containing font definitions grouped by file
|
||||
*
|
||||
* @var array<string, array{
|
||||
* 'dir': string,
|
||||
* 'keys': array<string>,
|
||||
* 'length1': int,
|
||||
* 'length2': int,
|
||||
* 'subset': bool,
|
||||
* }>
|
||||
*/
|
||||
protected array $file = [];
|
||||
|
||||
/**
|
||||
* Optional file helper forwarded to font loaders.
|
||||
*
|
||||
* @var ObjFile|null
|
||||
*/
|
||||
protected ?ObjFile $fileHelper;
|
||||
|
||||
/**
|
||||
* Initialize fonts buffer
|
||||
*
|
||||
* @param float $kunit Unit of measure conversion ratio.
|
||||
* @param bool $subset If true embed only a subset of the fonts
|
||||
* (stores only the information related to
|
||||
* the used characters); If false embed
|
||||
* full font; This option is valid only for
|
||||
* TrueTypeUnicode fonts and is disabled
|
||||
* for PDF/A. If you want to enable users to
|
||||
* modify the document, set this parameter
|
||||
* to false. If you subset the font, the
|
||||
* person who receives your PDF would need
|
||||
* to have your same font in order to make
|
||||
* changes to your PDF. The file size of the
|
||||
* PDF would also be smaller because you are
|
||||
* embedding only a subset. NOTE: This
|
||||
* option is computational and memory
|
||||
* intensive.
|
||||
* @param bool $unicode True if we are in Unicode mode, False otherwise.
|
||||
* @param bool $pdfa True if we are in PDF/A mode, False otherwise.
|
||||
* @param ObjFile|null $fileHelper Optional file helper for font loading.
|
||||
*/
|
||||
public function __construct(
|
||||
protected float $kunit,
|
||||
protected bool $subset = false,
|
||||
protected bool $unicode = true,
|
||||
protected bool $pdfa = false,
|
||||
?ObjFile $fileHelper = null,
|
||||
) {
|
||||
$this->fileHelper = $fileHelper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default subset mode
|
||||
*/
|
||||
public function isSubsetMode(): bool
|
||||
{
|
||||
return $this->subset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the fonts buffer
|
||||
*
|
||||
* @return array<string, TFontData>
|
||||
*/
|
||||
public function getFonts(): array
|
||||
{
|
||||
return $this->font;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the fonts buffer
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function getEncDiffs(): array
|
||||
{
|
||||
return $this->encdiff;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the specified font key exist on buffer
|
||||
*
|
||||
* @param string $key Font key
|
||||
*/
|
||||
public function isValidKey(string $key): bool
|
||||
{
|
||||
return isset($this->font[$key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get font by key
|
||||
*
|
||||
* @param string $key Font key
|
||||
*
|
||||
* @return TFontData Returns the fonts array.
|
||||
*
|
||||
* @throws FontException in case of error
|
||||
*/
|
||||
public function getFont(string $key): array
|
||||
{
|
||||
if (!isset($this->font[$key])) {
|
||||
throw new FontException('The font ' . $key . ' has not been loaded');
|
||||
}
|
||||
|
||||
return $this->font[$key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a character to the subset list
|
||||
*
|
||||
* @param string $key The font key
|
||||
* @param int $char The Unicode character value to add
|
||||
*
|
||||
* @throws FontException
|
||||
*/
|
||||
public function addSubsetChar(string $key, int $char): void
|
||||
{
|
||||
if (!isset($this->font[$key])) {
|
||||
throw new FontException('The font ' . $key . ' has not been loaded');
|
||||
}
|
||||
|
||||
$this->font[$key]['subsetchars'][$char] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new font to the fonts buffer
|
||||
*
|
||||
* The definition file (and the font file itself when embedding) must be present either in the current directory
|
||||
* or in the one indicated by K_PATH_FONTS if the constant is defined.
|
||||
*
|
||||
* @param int $objnum Current PDF object number
|
||||
* @param string $font Font family.
|
||||
* If it is a standard family name, it will override the corresponding font.
|
||||
* @param string $style Font style.
|
||||
* Possible values are (case-insensitive):
|
||||
* regular (default)
|
||||
* B: bold
|
||||
* I: italic
|
||||
* U: underline
|
||||
* D: strikeout (linethrough)
|
||||
* O: overline
|
||||
* @param string $ifile The font definition file (or empty for autodetect).
|
||||
* By default, the name is built from the family and style, in lower case with no spaces.
|
||||
* @param ?bool $subset If true embed only a subset of the font
|
||||
* (stores only the information related to
|
||||
* the used characters); If false embed
|
||||
* full font; This option is valid only
|
||||
* for TrueTypeUnicode fonts and is
|
||||
* disabled for PDF/A. If you want to
|
||||
* enable users to modify the document,
|
||||
* set this parameter to false. If you
|
||||
* subset the font, the person who
|
||||
* receives your PDF would need to have
|
||||
* your same font in order to make changes
|
||||
* to your PDF. The file size of the PDF
|
||||
* would also be smaller because you are
|
||||
* embedding only a subset. Set this to
|
||||
* null to use the default value. NOTE:
|
||||
* This option is computational and memory
|
||||
* intensive.
|
||||
*
|
||||
* @return string Font key
|
||||
*
|
||||
* @throws FontException in case of error
|
||||
*/
|
||||
public function add(
|
||||
int &$objnum,
|
||||
string $font,
|
||||
string $style = '',
|
||||
string $ifile = '',
|
||||
?bool $subset = null,
|
||||
): string {
|
||||
if ($subset === null) {
|
||||
$subset = $this->subset;
|
||||
}
|
||||
|
||||
// The font key depends only on (family, style, unicode, pdfa) - all known without
|
||||
// constructing a Font. When autodetecting the definition file (ifile === '') and the
|
||||
// resolved font is already loaded, skip the expensive Font allocation.
|
||||
if ($ifile === '' && isset($this->fontKeyCache[$font][$style])) {
|
||||
$cachedKey = $this->fontKeyCache[$font][$style];
|
||||
if (isset($this->font[$cachedKey])) {
|
||||
return $cachedKey;
|
||||
}
|
||||
}
|
||||
|
||||
$fobj = new Font($font, $style, $ifile, $subset, $this->unicode, $this->pdfa, true, $this->fileHelper);
|
||||
$key = $fobj->getFontkey();
|
||||
if ($ifile === '') {
|
||||
$this->fontKeyCache[$font][$style] = $key;
|
||||
}
|
||||
|
||||
if (isset($this->font[$key])) {
|
||||
return $key;
|
||||
}
|
||||
|
||||
$fobj->load();
|
||||
$this->font[$key] = $fobj->getFontData();
|
||||
|
||||
$this->setFontFile($key);
|
||||
$this->setFontDiff($key);
|
||||
|
||||
$this->font[$key]['i'] = ++$this->numfonts;
|
||||
$this->font[$key]['n'] = ++$objnum; // @phpstan-ignore assign.propertyType
|
||||
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set font file and subset
|
||||
*
|
||||
* @param string $key Font key
|
||||
*/
|
||||
protected function setFontFile(string $key): void
|
||||
{
|
||||
if ($this->font[$key]['file'] === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$file = $this->font[$key]['file'];
|
||||
if (!isset($this->file[$file])) {
|
||||
$this->file[$file] = [
|
||||
'dir' => '',
|
||||
'keys' => [],
|
||||
'length1' => 0,
|
||||
'length2' => 0,
|
||||
// a shared font file may only be subset if every font referencing it is subset
|
||||
'subset' => $this->font[$key]['subset'],
|
||||
];
|
||||
} else {
|
||||
$this->file[$file]['subset'] = $this->file[$file]['subset'] && $this->font[$key]['subset'];
|
||||
}
|
||||
|
||||
if (!\in_array($key, $this->file[$file]['keys'], true)) {
|
||||
$this->file[$file]['keys'][] = $key;
|
||||
}
|
||||
|
||||
$this->file[$file]['dir'] = $this->font[$key]['dir'];
|
||||
$this->file[$file]['length1'] = $this->font[$key]['length1'];
|
||||
$this->file[$file]['length2'] = $this->font[$key]['length2'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set font diff
|
||||
*
|
||||
* @param string $key Font key
|
||||
*/
|
||||
protected function setFontDiff(string $key): void
|
||||
{
|
||||
if ($this->font[$key]['diff'] === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$diffid = \array_search($this->font[$key]['diff'], $this->encdiff, true);
|
||||
if ($diffid === false) {
|
||||
$diffid = ++$this->numdiffs;
|
||||
$this->encdiff[$diffid] = $this->font[$key]['diff'];
|
||||
}
|
||||
|
||||
$this->font[$key]['diffid'] = $diffid;
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Com\Tecnick\Pdf\Font\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
|
||||
*/
|
||||
class Core
|
||||
{
|
||||
/**
|
||||
* Core fonts
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public const FONT = [
|
||||
'courier' => 'Courier',
|
||||
'courierB' => 'Courier-Bold',
|
||||
'courierI' => 'Courier-Oblique',
|
||||
'courierBI' => 'Courier-BoldOblique',
|
||||
'helvetica' => 'Helvetica',
|
||||
'helveticaB' => 'Helvetica-Bold',
|
||||
'helveticaI' => 'Helvetica-Oblique',
|
||||
'helveticaBI' => 'Helvetica-BoldOblique',
|
||||
'timesroman' => 'Times-Roman',
|
||||
'times' => 'Times-Roman',
|
||||
'timesB' => 'Times-Bold',
|
||||
'timesI' => 'Times-Italic',
|
||||
'timesBI' => 'Times-BoldItalic',
|
||||
'symbol' => 'Symbol',
|
||||
'symbolB' => 'Symbol',
|
||||
'symbolI' => 'Symbol',
|
||||
'symbolBI' => 'Symbol',
|
||||
'zapfdingbats' => 'ZapfDingbats',
|
||||
'zapfdingbatsB' => 'ZapfDingbats',
|
||||
'zapfdingbatsI' => 'ZapfDingbats',
|
||||
'zapfdingbatsBI' => 'ZapfDingbats',
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Exception.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;
|
||||
|
||||
/**
|
||||
* Com\Tecnick\Pdf\Font\Exception
|
||||
*
|
||||
* Custom Exception class
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
class Exception extends \Exception {}
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Font.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;
|
||||
|
||||
use Com\Tecnick\File\File as ObjFile;
|
||||
use Com\Tecnick\Pdf\Font\Exception as FontException;
|
||||
|
||||
/**
|
||||
* Com\Tecnick\Pdf\Font\Font
|
||||
*
|
||||
* @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 Load
|
||||
*/
|
||||
class Font extends \Com\Tecnick\Pdf\Font\Load
|
||||
{
|
||||
/**
|
||||
* Load an imported font
|
||||
*
|
||||
* The definition file (and the font file itself when embedding) must be present either in the current directory
|
||||
* or in the one indicated by K_PATH_FONTS if the constant is defined.
|
||||
*
|
||||
* @param string $font Font family.
|
||||
* If it is a standard family name, it will override the corresponding font.
|
||||
* @param string $style Font style. Possible values are (case-insensitive):
|
||||
* regular (default)
|
||||
* B: bold
|
||||
* I: italic
|
||||
* U: underline
|
||||
* D: strikeout (linethrough)
|
||||
* O: overline
|
||||
* @param string $ifile The font definition file (or empty for autodetect).
|
||||
* By default, the name is built from the family and
|
||||
* style, in lower case with no spaces.
|
||||
* @param bool $subset If true embed only a subset of the font
|
||||
* (stores only the information related to
|
||||
* the used characters); If false embed
|
||||
* full font; This option is valid only for
|
||||
* TrueTypeUnicode fonts and is disabled
|
||||
* for PDF/A. If you want to enable users
|
||||
* to modify the document, set this
|
||||
* parameter to false. If you subset the
|
||||
* font, the person who receives your PDF
|
||||
* would need to have your same font in
|
||||
* order to make changes to your PDF. The
|
||||
* file size of the PDF would also be
|
||||
* smaller because you are embedding only a subset.
|
||||
* @param bool $unicode True in Unicode mode, False otherwise.
|
||||
* @param bool $pdfa True in PDF/A mode, False otherwise.
|
||||
* @param bool $compress Set to false to disable stream compression.
|
||||
* @param ObjFile|null $fileHelper Optional file helper for font loading.
|
||||
*
|
||||
* @throws FontException in case of error
|
||||
*/
|
||||
public function __construct(
|
||||
string $font,
|
||||
string $style = '',
|
||||
string $ifile = '',
|
||||
bool $subset = false,
|
||||
bool $unicode = true,
|
||||
bool $pdfa = false,
|
||||
bool $compress = true,
|
||||
?ObjFile $fileHelper = null,
|
||||
) {
|
||||
parent::__construct($fileHelper);
|
||||
|
||||
if ($font === '') {
|
||||
throw new FontException('empty font family name');
|
||||
}
|
||||
|
||||
if ($ifile !== '') {
|
||||
$validatedIfile = $ifile;
|
||||
if (!$this->fileHelper->isValidFile($validatedIfile)) {
|
||||
throw new FontException('Invalid font ifile: ' . $ifile);
|
||||
}
|
||||
}
|
||||
|
||||
$this->data['ifile'] = $ifile;
|
||||
$this->data['family'] = $font;
|
||||
$this->data['unicode'] = $unicode;
|
||||
$this->data['pdfa'] = $pdfa;
|
||||
$this->data['compress'] = $compress;
|
||||
$this->data['subset'] = $subset;
|
||||
$this->data['subsetchars'] = \array_fill(0, 256, true);
|
||||
|
||||
// generate the font key and set styles
|
||||
$this->setStyle($style);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the font key
|
||||
*/
|
||||
public function getFontkey(): string
|
||||
{
|
||||
return $this->data['key'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the font data
|
||||
*
|
||||
* @return TFontData
|
||||
*/
|
||||
public function getFontData(): array
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set style and normalize the font name
|
||||
*
|
||||
* @param string $style Style
|
||||
*/
|
||||
protected function setStyle(string $style): void
|
||||
{
|
||||
$style = \strtoupper($style);
|
||||
if (\str_ends_with($this->data['family'], 'I')) {
|
||||
$style .= 'I';
|
||||
$this->data['family'] = \substr($this->data['family'], 0, -1);
|
||||
}
|
||||
|
||||
if (\str_ends_with($this->data['family'], 'B')) {
|
||||
$style .= 'B';
|
||||
$this->data['family'] = \substr($this->data['family'], 0, -1);
|
||||
}
|
||||
|
||||
// normalize family name
|
||||
$this->data['family'] = \strtolower($this->data['family']);
|
||||
if (!$this->data['unicode'] && $this->data['family'] === 'arial') {
|
||||
$this->data['family'] = 'helvetica';
|
||||
}
|
||||
|
||||
if ($this->data['family'] === 'symbol' || $this->data['family'] === 'zapfdingbats') {
|
||||
$style = '';
|
||||
}
|
||||
|
||||
if ($this->data['pdfa'] && isset(Core::FONT[$this->data['family']])) {
|
||||
// core fonts must be embedded in PDF/A
|
||||
$this->data['family'] = 'pdfa' . $this->data['family'];
|
||||
}
|
||||
|
||||
$this->setStyleMode($style);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set style mode properties
|
||||
*
|
||||
* @param string $style Style
|
||||
*/
|
||||
protected function setStyleMode(string $style): void
|
||||
{
|
||||
$suffix = '';
|
||||
if (\str_contains($style, 'B')) {
|
||||
$this->data['mode']['bold'] = true;
|
||||
$suffix .= 'B';
|
||||
}
|
||||
|
||||
if (\str_contains($style, 'I')) {
|
||||
$this->data['mode']['italic'] = true;
|
||||
$suffix .= 'I';
|
||||
}
|
||||
|
||||
$this->data['style'] = $suffix;
|
||||
if (\str_contains($style, 'U')) {
|
||||
$this->data['style'] .= 'U';
|
||||
$this->data['mode']['underline'] = true;
|
||||
}
|
||||
|
||||
if (\str_contains($style, 'D')) {
|
||||
$this->data['style'] .= 'D';
|
||||
$this->data['mode']['linethrough'] = true;
|
||||
}
|
||||
|
||||
if (\str_contains($style, 'O')) {
|
||||
$this->data['style'] .= 'O';
|
||||
$this->data['mode']['overline'] = true;
|
||||
}
|
||||
|
||||
$this->data['key'] = $this->data['family'] . $suffix;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* FontPaths.php
|
||||
*
|
||||
* @since 2026-06-08
|
||||
* @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;
|
||||
|
||||
/**
|
||||
* Shared font paths rooted at the library base directory.
|
||||
*/
|
||||
class FontPaths
|
||||
{
|
||||
/**
|
||||
* Returns the library root directory.
|
||||
*/
|
||||
public static function getLibraryRoot(): string
|
||||
{
|
||||
return \rtrim(\dirname(__DIR__), '/\\');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default input font directory.
|
||||
*/
|
||||
public static function getInputPath(): string
|
||||
{
|
||||
return self::getLibraryRoot() . '/fonts';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default output font directory.
|
||||
*/
|
||||
public static function getOutputPath(): string
|
||||
{
|
||||
return self::getLibraryRoot() . '/target/fonts';
|
||||
}
|
||||
|
||||
/**
|
||||
* Build trusted roots for local font file access.
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public static function buildAllowedPaths(): array
|
||||
{
|
||||
$roots = [
|
||||
self::getInputPath(),
|
||||
self::getOutputPath(),
|
||||
];
|
||||
|
||||
if (\defined('K_PATH_FONTS')) {
|
||||
$kpathfonts = (string) \constant('K_PATH_FONTS');
|
||||
if ($kpathfonts !== '') {
|
||||
$roots[] = $kpathfonts;
|
||||
}
|
||||
}
|
||||
|
||||
$allowed = [];
|
||||
foreach ($roots as $root) {
|
||||
$normalized = \rtrim($root, '/\\');
|
||||
if ($normalized === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$allowed[] = $normalized;
|
||||
|
||||
$resolved = \realpath($normalized);
|
||||
if ($resolved !== false) {
|
||||
$allowed[] = \rtrim($resolved, '/\\');
|
||||
}
|
||||
}
|
||||
|
||||
return \array_values(\array_unique($allowed));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* FontSubsetCacheInterface.php
|
||||
*
|
||||
* @since 2026-06-16
|
||||
* @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;
|
||||
|
||||
/**
|
||||
* Com\Tecnick\Pdf\Font\FontSubsetCacheInterface
|
||||
*
|
||||
* Optional cache contract for reusing TrueType font subset programs.
|
||||
*
|
||||
* Implementations are injected into Output and consulted before the
|
||||
* (computational and memory intensive) subsetting is performed. The cached
|
||||
* value is the raw subset font program string, i.e. the output of
|
||||
* Subset::getSubsetFont() before any compression or encryption.
|
||||
*
|
||||
* The library never evicts entries; backends own their own expiration and
|
||||
* size limits.
|
||||
*
|
||||
* @since 2026-06-16
|
||||
* @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
|
||||
*/
|
||||
interface FontSubsetCacheInterface
|
||||
{
|
||||
/**
|
||||
* Return the cached subset font program for the given key, or null on miss.
|
||||
*
|
||||
* @param string $key Cache key identifying the font subset.
|
||||
*/
|
||||
public function get(string $key): ?string;
|
||||
|
||||
/**
|
||||
* Store the subset font program for the given key.
|
||||
*
|
||||
* @param string $key Cache key identifying the font subset.
|
||||
* @param string $subsetFont Raw subset font program (uncompressed).
|
||||
*/
|
||||
public function set(string $key, string $subsetFont): void;
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* FontType.php
|
||||
*
|
||||
* @since 2026-07-17
|
||||
* @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;
|
||||
|
||||
use Com\Tecnick\Pdf\Font\Exception as FontException;
|
||||
|
||||
/**
|
||||
* Com\Tecnick\Pdf\Font\FontType
|
||||
*
|
||||
* Backed enum for the font type accepted by Import::__construct. The backing
|
||||
* value of each case is the canonical type name; the empty string selects
|
||||
* autodetection.
|
||||
*
|
||||
* @since 2026-07-17
|
||||
* @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
|
||||
*/
|
||||
enum FontType: string
|
||||
{
|
||||
/** Autodetect the font type from the file contents. */
|
||||
case Auto = '';
|
||||
|
||||
/** Adobe Font Metrics (one of the 14 Core fonts). */
|
||||
case Core = 'Core';
|
||||
|
||||
case TrueType = 'TrueType';
|
||||
|
||||
case TrueTypeUnicode = 'TrueTypeUnicode';
|
||||
|
||||
case Type1 = 'Type1';
|
||||
|
||||
/** CID-0 Japanese. */
|
||||
case Cid0Jp = 'CID0JP';
|
||||
|
||||
/** CID-0 Korean. */
|
||||
case Cid0Kr = 'CID0KR';
|
||||
|
||||
/** CID-0 Chinese Simplified. */
|
||||
case Cid0Cs = 'CID0CS';
|
||||
|
||||
/** CID-0 Chinese Traditional. */
|
||||
case Cid0Ct = 'CID0CT';
|
||||
|
||||
/**
|
||||
* Resolve a loose font type value to the matching enum case.
|
||||
*
|
||||
* Accepts the canonical type name (or the empty string for autodetection)
|
||||
* or an enum instance (returned unchanged). Unknown values throw, matching
|
||||
* the closed set validated by Import::getFontType().
|
||||
*
|
||||
* @param string|self $value Font type name or enum case.
|
||||
*
|
||||
* @throws FontException if the value does not match a known font type.
|
||||
*/
|
||||
public static function fromLoose(string|self $value): self
|
||||
{
|
||||
if ($value instanceof self) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return self::tryFrom($value) ?? throw new FontException('unknown or unsupported font type: ' . $value);
|
||||
}
|
||||
}
|
||||
+741
@@ -0,0 +1,741 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Import.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;
|
||||
|
||||
use Com\Tecnick\File\Byte;
|
||||
use Com\Tecnick\File\Dir;
|
||||
use Com\Tecnick\File\Exception as FileException;
|
||||
use Com\Tecnick\File\File as ObjFile;
|
||||
use Com\Tecnick\Pdf\Font\Exception as FontException;
|
||||
use Com\Tecnick\Pdf\Font\Import\Core;
|
||||
use Com\Tecnick\Pdf\Font\Import\TrueType;
|
||||
use Com\Tecnick\Pdf\Font\Import\TypeOne;
|
||||
use Com\Tecnick\Unicode\Data\Encoding;
|
||||
|
||||
/**
|
||||
* Com\Tecnick\Pdf\Font\Import
|
||||
*
|
||||
* @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 Load
|
||||
*
|
||||
* @SuppressWarnings("PHPMD.ExcessiveClassComplexity")
|
||||
*/
|
||||
class Import
|
||||
{
|
||||
/**
|
||||
* File helper used to load font definition files.
|
||||
*/
|
||||
protected ObjFile $fileHelper;
|
||||
|
||||
/**
|
||||
* True when the file helper is created internally by this class.
|
||||
*/
|
||||
protected bool $ownsFileHelper = false;
|
||||
|
||||
/**
|
||||
* Content of the input font file
|
||||
*/
|
||||
protected string $font = '';
|
||||
|
||||
/**
|
||||
* Object used to read font bytes
|
||||
*/
|
||||
protected Byte $fbyte;
|
||||
|
||||
/**
|
||||
* Extracted font metrics
|
||||
*
|
||||
* @var TFontData
|
||||
*/
|
||||
protected array $fdt = [
|
||||
'Ascender' => 0,
|
||||
'Ascent' => 0,
|
||||
'AvgWidth' => 0.0,
|
||||
'CapHeight' => 0,
|
||||
'CharacterSet' => '',
|
||||
'Descender' => 0,
|
||||
'Descent' => 0,
|
||||
'EncodingScheme' => '',
|
||||
'FamilyName' => '',
|
||||
'Flags' => 0,
|
||||
'FontBBox' => [],
|
||||
'FontName' => '',
|
||||
'FullName' => '',
|
||||
'IsFixedPitch' => false,
|
||||
'ItalicAngle' => 0,
|
||||
'Leading' => 0,
|
||||
'MaxWidth' => 0,
|
||||
'MissingWidth' => 0,
|
||||
'StdHW' => 0,
|
||||
'StdVW' => 0,
|
||||
'StemH' => 0,
|
||||
'StemV' => 0,
|
||||
'UnderlinePosition' => 0,
|
||||
'UnderlineThickness' => 0,
|
||||
'Version' => '',
|
||||
'Weight' => '',
|
||||
'XHeight' => 0,
|
||||
'bbox' => '',
|
||||
'cbbox' => [],
|
||||
'cidinfo' => [
|
||||
'Ordering' => '',
|
||||
'Registry' => '',
|
||||
'Supplement' => 0,
|
||||
'uni2cid' => [],
|
||||
],
|
||||
'compress' => false,
|
||||
'ctg' => '',
|
||||
'ctgdata' => [],
|
||||
'cw' => [],
|
||||
'cwu' => [],
|
||||
'datafile' => '',
|
||||
'desc' => [
|
||||
'Ascent' => 0,
|
||||
'AvgWidth' => 0,
|
||||
'CapHeight' => 0,
|
||||
'Descent' => 0,
|
||||
'Flags' => 0,
|
||||
'FontBBox' => '',
|
||||
'ItalicAngle' => 0,
|
||||
'Leading' => 0,
|
||||
'MaxWidth' => 0,
|
||||
'MissingWidth' => 0,
|
||||
'StemH' => 0,
|
||||
'StemV' => 0,
|
||||
'XHeight' => 0,
|
||||
],
|
||||
'diff' => '',
|
||||
'diff_n' => 0,
|
||||
'dir' => '',
|
||||
'dw' => 0,
|
||||
'enc' => '',
|
||||
'enc_map' => [],
|
||||
'encodingTables' => [],
|
||||
'encoding_id' => 0,
|
||||
'encrypted' => '',
|
||||
'fakestyle' => false,
|
||||
'family' => '',
|
||||
'file' => '',
|
||||
'file_n' => 0,
|
||||
'file_name' => '',
|
||||
'i' => 0,
|
||||
'ifile' => '',
|
||||
'indexToLoc' => [],
|
||||
'input_file' => '',
|
||||
'isUnicode' => false,
|
||||
'italicAngle' => 0,
|
||||
'key' => '',
|
||||
'lenIV' => 0,
|
||||
'length1' => 0,
|
||||
'length2' => 0,
|
||||
'linked' => false,
|
||||
'mode' => [
|
||||
'bold' => false,
|
||||
'italic' => false,
|
||||
'linethrough' => false,
|
||||
'overline' => false,
|
||||
'underline' => false,
|
||||
],
|
||||
'n' => 0,
|
||||
'name' => '',
|
||||
'numGlyphs' => 0,
|
||||
'numHMetrics' => 0,
|
||||
'originalsize' => 0,
|
||||
'pdfa' => false,
|
||||
'platform_id' => 0,
|
||||
'settype' => '',
|
||||
'short_offset' => false,
|
||||
'size1' => 0,
|
||||
'size2' => 0,
|
||||
'style' => '',
|
||||
'subset' => false,
|
||||
'subsetchars' => [],
|
||||
'table' => [],
|
||||
'tot_num_glyphs' => 0,
|
||||
'type' => '',
|
||||
'underlinePosition' => 0,
|
||||
'underlineThickness' => 0,
|
||||
'unicode' => false,
|
||||
'unitsPerEm' => 0,
|
||||
'up' => 0,
|
||||
'urk' => 0.0,
|
||||
'ut' => 0,
|
||||
'weight' => '',
|
||||
];
|
||||
|
||||
/**
|
||||
* Import the specified font and create output files.
|
||||
*
|
||||
* @param string $file Font file to process
|
||||
* @param string $output_path Output path for generated font files (must be writeable by the web server).
|
||||
* Leave null for default font folder.
|
||||
* @param string|FontType $type Font type (or FontType enum case). Leave empty for autodetect mode.
|
||||
* Valid values are:
|
||||
* Core (AFM - Adobe Font Metrics) TrueTypeUnicode TrueType
|
||||
* Type1 CID0JP (CID-0 Japanese) CID0KR (CID-0 Korean) CID0CS
|
||||
* (CID-0 Chinese Simplified) CID0CT (CID-0 Chinese Traditional)
|
||||
* @param string $encoding Name of the encoding table to use. Leave empty for default mode.
|
||||
* Omit this parameter for TrueType Unicode and symbolic fonts like
|
||||
* Symbol or ZapfDingBats.
|
||||
* @param int $flags Unsigned 32-bit integer containing flags specifying various characteristics
|
||||
* of the font as described in "PDF32000:2008 - 9.8.2 Font Descriptor Flags":
|
||||
* +1 for fixed width font +4 for symbol or +32 for non-symbol +64 for italic
|
||||
* Note: Fixed and Italic mode are generally autodetected, so you have to set
|
||||
* it to 32 = non-symbolic font (default) or 4 = symbolic font.
|
||||
* @param int $platform_id Platform ID for CMAP table to extract.
|
||||
* For a Unicode font for Windows this
|
||||
* value should be 3, for Macintosh
|
||||
* should be 1.
|
||||
* @param int $encoding_id Encoding ID for CMAP table to extract.
|
||||
* For a Unicode font for Windows this
|
||||
* value should be 1, for Macintosh
|
||||
* should be 0. When Platform ID is 3,
|
||||
* legal values for Encoding ID are: 0 =
|
||||
* Symbol, 1 = Unicode, 2 = ShiftJIS, 3 =
|
||||
* PRC, 4 = Big5, 5 = Wansung, 6 = Johab,
|
||||
* 7 = Reserved, 8 = Reserved, 9 =
|
||||
* Reserved, 10 = UCS-4.
|
||||
* @param bool $linked If true, links the font file to system font instead of copying the font data
|
||||
* (not transportable). Note: this option do not work with Type1 fonts.
|
||||
* @param ObjFile|null $fileHelper Optional file helper for font loading.
|
||||
*
|
||||
* @throws FileException in case of error
|
||||
* @throws FontException in case of error
|
||||
* @throws \RangeException in case of byte-range errors
|
||||
*/
|
||||
public function __construct(
|
||||
string $file,
|
||||
string $output_path = '',
|
||||
string|FontType $type = '',
|
||||
string $encoding = '',
|
||||
int $flags = 32,
|
||||
int $platform_id = 3,
|
||||
int $encoding_id = 1,
|
||||
bool $linked = false,
|
||||
?ObjFile $fileHelper = null,
|
||||
) {
|
||||
$this->ownsFileHelper = $fileHelper === null;
|
||||
$this->fileHelper = $fileHelper ?? new ObjFile(allowedPaths: self::buildAllowedPaths($file));
|
||||
$validatedFile = $file;
|
||||
if (!$this->fileHelper->isValidFile($validatedFile)) {
|
||||
throw new FontException('Invalid font file name: ' . $file);
|
||||
}
|
||||
|
||||
$this->fdt['input_file'] = $file;
|
||||
$this->fdt['file_name'] = $this->makeFontName($file);
|
||||
if ($this->fdt['file_name'] === '') {
|
||||
throw new FontException('the font name is empty');
|
||||
}
|
||||
|
||||
$this->fdt['dir'] = $this->findOutputPath($output_path);
|
||||
if ($this->ownsFileHelper) {
|
||||
$this->fileHelper->setAllowedPaths(self::buildAllowedPaths($file, $this->fdt['dir']));
|
||||
}
|
||||
|
||||
$this->fdt['datafile'] = $this->fdt['dir'] . $this->fdt['file_name'] . '.json';
|
||||
if (\file_exists($this->fdt['datafile'])) {
|
||||
throw new FontException('this font has been already imported: ' . $this->fdt['datafile']);
|
||||
}
|
||||
|
||||
// get font data
|
||||
if (!is_file($file)) {
|
||||
throw new FontException('invalid font file: ' . $file);
|
||||
}
|
||||
|
||||
if (($font = $this->fileHelper->getLocalFileData($file)) === false) {
|
||||
throw new FontException('unable to read the input font file: ' . $file);
|
||||
}
|
||||
|
||||
$this->font = $font;
|
||||
|
||||
$this->fbyte = new Byte($this->font);
|
||||
|
||||
if ($type instanceof FontType) {
|
||||
$type = $type->value;
|
||||
}
|
||||
|
||||
$this->fdt['settype'] = $type;
|
||||
$this->fdt['type'] = $this->getFontType($type);
|
||||
$this->fdt['isUnicode'] = $this->fdt['type'] === 'TrueTypeUnicode' || $this->fdt['type'] === 'cidfont0';
|
||||
$this->fdt['Flags'] = $flags;
|
||||
$this->initFlags();
|
||||
$this->fdt['enc'] = $this->getEncodingTable($encoding);
|
||||
$this->fdt['diff'] = $this->getEncodingDiff();
|
||||
$this->fdt['originalsize'] = \strlen($this->font);
|
||||
$this->fdt['ctg'] = $this->fdt['file_name'] . '.ctg.z';
|
||||
$this->fdt['platform_id'] = $platform_id;
|
||||
$this->fdt['encoding_id'] = $encoding_id;
|
||||
$this->fdt['linked'] = $linked;
|
||||
|
||||
$processor = match ($this->fdt['type']) {
|
||||
'Core' => new Core(font: $this->font, fdt: $this->fdt, fileHelper: $this->fileHelper),
|
||||
'Type1' => new TypeOne(font: $this->font, fdt: $this->fdt, fileHelper: $this->fileHelper),
|
||||
default => new TrueType(
|
||||
font: $this->font,
|
||||
fdt: $this->fdt,
|
||||
fileHelper: $this->fileHelper,
|
||||
fbyte: $this->fbyte,
|
||||
),
|
||||
};
|
||||
|
||||
$this->fdt = $processor->getFontMetrics();
|
||||
|
||||
$this->saveFontData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the extracted font metrics
|
||||
*
|
||||
* @return TFontData
|
||||
*/
|
||||
public function getFontMetrics(): array
|
||||
{
|
||||
return $this->fdt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the output font name
|
||||
*/
|
||||
public function getFontName(): string
|
||||
{
|
||||
return $this->fdt['file_name'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize font flags from font name
|
||||
*/
|
||||
protected function initFlags(): void
|
||||
{
|
||||
$filename = \strtolower(\basename($this->fdt['input_file']));
|
||||
|
||||
if (
|
||||
\str_contains($filename, 'mono')
|
||||
|| \str_contains($filename, 'courier')
|
||||
|| \str_contains($filename, 'fixed')
|
||||
) {
|
||||
$this->fdt['Flags'] |= 1;
|
||||
}
|
||||
|
||||
if (\str_contains($filename, 'symbol') || \str_contains($filename, 'zapfdingbats')) {
|
||||
$this->fdt['Flags'] |= 4;
|
||||
}
|
||||
|
||||
if (\str_contains($filename, 'italic') || \str_contains($filename, 'oblique')) {
|
||||
$this->fdt['Flags'] |= 64;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for unsafe path components that were previously rejected by the
|
||||
* file helper's internal validation.
|
||||
*/
|
||||
private static function hasUnsafePath(string $path): bool
|
||||
{
|
||||
return (
|
||||
$path !== ''
|
||||
&& (
|
||||
\str_contains($path, '://')
|
||||
|| \str_contains(\str_ireplace('%2E', '.', \html_entity_decode($path, ENT_QUOTES, 'UTF-8')), '..')
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build trusted roots for local file validation.
|
||||
*
|
||||
* The minimum roots required by Import are:
|
||||
* - the input font directory (read access)
|
||||
* - the output directory (write access), when available
|
||||
*
|
||||
* For each root we include both the given path and, when resolvable,
|
||||
* its canonical realpath to support symlinked directories.
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
private static function buildAllowedPaths(string $fontFile, string $outputDir = ''): array
|
||||
{
|
||||
$roots = [];
|
||||
|
||||
$fontDir = \dirname($fontFile);
|
||||
if ($fontDir !== '' && $fontDir !== '.') {
|
||||
$roots[] = $fontDir;
|
||||
}
|
||||
|
||||
if ($outputDir !== '') {
|
||||
$roots[] = $outputDir;
|
||||
}
|
||||
|
||||
$allowed = [];
|
||||
foreach ($roots as $root) {
|
||||
$normalized = \rtrim($root, '/\\');
|
||||
if ($normalized === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$allowed[] = $normalized;
|
||||
|
||||
$resolved = \realpath($normalized);
|
||||
if ($resolved !== false) {
|
||||
$allowed[] = \rtrim($resolved, '/\\');
|
||||
}
|
||||
}
|
||||
|
||||
return \array_values(\array_unique($allowed));
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the exported metadata font file
|
||||
*
|
||||
* @throws FileException
|
||||
* @throws FontException
|
||||
*
|
||||
* @SuppressWarnings("PHPMD.CyclomaticComplexity")
|
||||
* @SuppressWarnings("PHPMD.NPathComplexity")
|
||||
* @SuppressWarnings("PHPMD.ExcessiveMethodLength")
|
||||
*/
|
||||
protected function saveFontData(): void
|
||||
{
|
||||
$missingWidth = $this->fdt['MissingWidth'];
|
||||
$pfile =
|
||||
'{"type":"'
|
||||
. $this->fdt['type']
|
||||
. '"'
|
||||
. ',"name":"'
|
||||
. $this->fdt['name']
|
||||
. '"'
|
||||
. ',"up":'
|
||||
. $this->fdt['underlinePosition']
|
||||
. ',"ut":'
|
||||
. $this->fdt['underlineThickness']
|
||||
. ',"dw":'
|
||||
. ($missingWidth !== null && $missingWidth > 0 ? $missingWidth : $this->fdt['AvgWidth'])
|
||||
. ',"diff":"'
|
||||
. $this->fdt['diff']
|
||||
. '"'
|
||||
. ',"platform_id":'
|
||||
. $this->fdt['platform_id']
|
||||
. ',"encoding_id":'
|
||||
. $this->fdt['encoding_id'];
|
||||
|
||||
if ($this->fdt['type'] === 'Core') {
|
||||
// Core
|
||||
$pfile .= ',"enc":""';
|
||||
} elseif ($this->fdt['type'] === 'Type1') {
|
||||
// Type 1
|
||||
$pfile .=
|
||||
',"enc":"'
|
||||
. $this->fdt['enc']
|
||||
. '"'
|
||||
. ',"file":"'
|
||||
. $this->fdt['file']
|
||||
. '"'
|
||||
. ',"size1":'
|
||||
. $this->fdt['size1']
|
||||
. ',"size2":'
|
||||
. $this->fdt['size2'];
|
||||
} else {
|
||||
$pfile .= ',"originalsize":' . $this->fdt['originalsize'];
|
||||
if ($this->fdt['type'] === 'cidfont0') {
|
||||
$pfile .= ',' . (UniToCid::TYPE[$this->fdt['settype']] ?? '');
|
||||
} else {
|
||||
// TrueType
|
||||
$pfile .=
|
||||
',"enc":"'
|
||||
. $this->fdt['enc']
|
||||
. '"'
|
||||
. ',"file":"'
|
||||
. $this->fdt['file']
|
||||
. '"'
|
||||
. ',"ctg":"'
|
||||
. $this->fdt['ctg']
|
||||
. '"';
|
||||
// create CIDToGIDMap
|
||||
$cidtogidmap = \str_pad('', 131_072, "\x00"); // (256 * 256 * 2) = 131072
|
||||
foreach ($this->fdt['ctgdata'] as $cid => $gid) {
|
||||
$cidtogidmap = $this->updateCIDtoGIDmap($cidtogidmap, (int) $cid, (int) $gid);
|
||||
}
|
||||
|
||||
// store compressed CIDToGIDMap
|
||||
$fpt = $this->fileHelper->fopenLocal($this->fdt['dir'] . $this->fdt['ctg'], 'wb');
|
||||
|
||||
$cmpr = \gzcompress($cidtogidmap);
|
||||
if ($cmpr === false) {
|
||||
throw new FontException('unable to compress CIDToGIDMap');
|
||||
}
|
||||
|
||||
\fwrite($fpt, $cmpr);
|
||||
\fclose($fpt);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->fdt['isUnicode']) {
|
||||
$pfile .= ',"isUnicode":true';
|
||||
} else {
|
||||
$pfile .= ',"isUnicode":false';
|
||||
}
|
||||
|
||||
$pfile .=
|
||||
',"desc":{"Flags":'
|
||||
. $this->fdt['Flags']
|
||||
. ',"FontBBox":"['
|
||||
. $this->fdt['bbox']
|
||||
. ']"'
|
||||
. ',"ItalicAngle":'
|
||||
. $this->fdt['italicAngle']
|
||||
. ',"Ascent":'
|
||||
. $this->fdt['Ascent']
|
||||
. ',"Descent":'
|
||||
. $this->fdt['Descent']
|
||||
. ',"Leading":'
|
||||
. $this->fdt['Leading']
|
||||
. ',"CapHeight":'
|
||||
. $this->fdt['CapHeight']
|
||||
. ',"XHeight":'
|
||||
. $this->fdt['XHeight']
|
||||
. ',"StemV":'
|
||||
. $this->fdt['StemV']
|
||||
. ',"StemH":'
|
||||
. $this->fdt['StemH']
|
||||
. ',"AvgWidth":'
|
||||
. $this->fdt['AvgWidth']
|
||||
. ',"MaxWidth":'
|
||||
. $this->fdt['MaxWidth']
|
||||
. ',"MissingWidth":'
|
||||
. (string) ($missingWidth ?? 0)
|
||||
. '}';
|
||||
if ($this->fdt['cbbox'] !== []) {
|
||||
$ccboxstr = '';
|
||||
foreach ($this->fdt['cbbox'] as $cid => $bbox) {
|
||||
$box = \array_pad(\array_values($bbox), 4, 0);
|
||||
$ccboxstr .= ',"' . $cid . '":[' . $box[0] . ',' . $box[1] . ',' . $box[2] . ',' . $box[3] . ']';
|
||||
}
|
||||
|
||||
$pfile .= ',"cbbox":{' . \substr($ccboxstr, 1) . '}';
|
||||
}
|
||||
|
||||
if ($this->fdt['cw'] !== []) {
|
||||
$cwstr = '';
|
||||
foreach ($this->fdt['cw'] as $cid => $width) {
|
||||
$cwstr .= ',"' . $cid . '":' . $width;
|
||||
}
|
||||
|
||||
$pfile .= ',"cw":{' . \substr($cwstr, 1) . '}';
|
||||
}
|
||||
|
||||
if ($this->fdt['cwu'] !== []) {
|
||||
$cwustr = '';
|
||||
foreach ($this->fdt['cwu'] as $codepoint => $width) {
|
||||
$cwustr .= ',"' . $codepoint . '":' . $width;
|
||||
}
|
||||
|
||||
$pfile .= ',"cwu":{' . \substr($cwustr, 1) . '}';
|
||||
}
|
||||
|
||||
$pfile .= '}' . "\n";
|
||||
|
||||
// store file
|
||||
$fpt = $this->fileHelper->fopenLocal($this->fdt['datafile'], 'wb');
|
||||
\fwrite($fpt, $pfile);
|
||||
\fclose($fpt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the output font name
|
||||
*
|
||||
* @param string $font_file Input font file
|
||||
*
|
||||
* @throws FontException
|
||||
*/
|
||||
protected function makeFontName(string $font_file): string
|
||||
{
|
||||
$font_path_parts = \pathinfo($font_file);
|
||||
if ($font_path_parts['filename'] === '') {
|
||||
throw new FontException('Invalid font file name: ' . $font_file);
|
||||
}
|
||||
|
||||
$fname = \preg_replace('/[^a-z0-9_]/', '', \strtolower($font_path_parts['filename']));
|
||||
if ($fname === null) {
|
||||
throw new FontException('Invalid font file name: ' . $font_file);
|
||||
}
|
||||
|
||||
return \str_replace(['bold', 'oblique', 'italic', 'regular'], ['b', 'i', 'i', ''], $fname);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the path where to store the processed font.
|
||||
*
|
||||
* @param string $output_path Output path for generated font files (must be writeable by the web server).
|
||||
* Leave null for default font folder (K_PATH_FONTS).
|
||||
*/
|
||||
protected function findOutputPath(string $output_path = ''): string
|
||||
{
|
||||
if ($output_path !== '' && !self::hasUnsafePath($output_path) && \is_writable($output_path)) {
|
||||
return $output_path;
|
||||
}
|
||||
|
||||
if (\defined('K_PATH_FONTS')) {
|
||||
$kpathfonts = (string) \constant('K_PATH_FONTS');
|
||||
if ($kpathfonts !== '' && \is_writable($kpathfonts)) {
|
||||
return $kpathfonts;
|
||||
}
|
||||
}
|
||||
|
||||
$dirobj = new Dir();
|
||||
$dir = $dirobj->findParentDir('fonts', __DIR__);
|
||||
if ($dir === '/') {
|
||||
$dir = \sys_get_temp_dir();
|
||||
}
|
||||
|
||||
if (!\str_ends_with($dir, '/')) {
|
||||
$dir .= '/';
|
||||
}
|
||||
|
||||
return $dir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the font type
|
||||
*
|
||||
* @param string $font_type Font type. Leave empty for autodetect mode.
|
||||
*
|
||||
* @throws FontException
|
||||
*/
|
||||
protected function getFontType(string $font_type): string
|
||||
{
|
||||
// autodetect font type
|
||||
if ($font_type === '') {
|
||||
if (\str_starts_with($this->font, 'StartFontMetrics')) {
|
||||
// AFM type - we use this type only for the 14 Core fonts
|
||||
return 'Core';
|
||||
}
|
||||
|
||||
if (\str_starts_with($this->font, 'OTTO')) {
|
||||
throw new FontException('Unsupported font format: OpenType with CFF data');
|
||||
}
|
||||
|
||||
if ($this->fbyte->getULong(0) === 0x1_0000) {
|
||||
return 'TrueTypeUnicode';
|
||||
}
|
||||
|
||||
return 'Type1';
|
||||
}
|
||||
|
||||
if (\str_starts_with($font_type, 'CID0')) {
|
||||
return 'cidfont0';
|
||||
}
|
||||
|
||||
if (\in_array($font_type, ['Core', 'Type1', 'TrueType', 'TrueTypeUnicode'], true)) {
|
||||
return $font_type;
|
||||
}
|
||||
|
||||
throw new FontException('unknown or unsupported font type: ' . $font_type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the encoding table
|
||||
*
|
||||
* @param string $encoding Name of the encoding table to use. Leave empty for default mode.
|
||||
* Omit this parameter for TrueType Unicode and symbolic fonts like
|
||||
* Symbol or ZapfDingBats.
|
||||
*
|
||||
* @throws FontException
|
||||
*/
|
||||
protected function getEncodingTable(string $encoding = ''): string
|
||||
{
|
||||
if ($encoding === '') {
|
||||
if ($this->fdt['type'] === 'Type1' && ($this->fdt['Flags'] & 4) === 0) {
|
||||
return 'cp1252';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
$enc = \preg_replace('/[^A-Za-z0-9_\-]/', '', $encoding);
|
||||
if ($enc === null) {
|
||||
throw new FontException('Invalid encoding name: ' . $encoding);
|
||||
}
|
||||
|
||||
return $enc;
|
||||
}
|
||||
|
||||
/**
|
||||
* If required, get differences between the reference encoding (cp1252) and the current encoding
|
||||
*
|
||||
* @SuppressWarnings("PHPMD.CyclomaticComplexity")
|
||||
*/
|
||||
protected function getEncodingDiff(): string
|
||||
{
|
||||
$diff = '';
|
||||
if (
|
||||
($this->fdt['type'] === 'TrueType' || $this->fdt['type'] === 'Type1')
|
||||
&& ($this->fdt['enc'] !== '' && $this->fdt['enc'] !== 'cp1252' && isset(Encoding::MAP[$this->fdt['enc']]))
|
||||
) {
|
||||
// build differences from reference encoding
|
||||
$enc_ref = Encoding::MAP['cp1252'] ?? [];
|
||||
$enc_target = Encoding::MAP[$this->fdt['enc']];
|
||||
$last = 0;
|
||||
for ($idx = 32; $idx <= 255; ++$idx) {
|
||||
$target = $enc_target[$idx] ?? '';
|
||||
$ref = $enc_ref[$idx] ?? '';
|
||||
if ($target === $ref) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($idx !== ($last + 1)) {
|
||||
$diff .= $idx . ' ';
|
||||
}
|
||||
|
||||
$last = $idx;
|
||||
$diff .= '/' . $target . ' ';
|
||||
}
|
||||
}
|
||||
|
||||
return $diff;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the CIDToGIDMap string with a new value
|
||||
*
|
||||
* The CIDToGIDMap is made up of 16-bit values mapping a zero-based
|
||||
* Character Identifier index to its zero-based glyph id index.
|
||||
*
|
||||
* @param string $map CIDToGIDMap (binary).
|
||||
* @param int $cid CID value.
|
||||
* @param int $gid GID value.
|
||||
*/
|
||||
protected function updateCIDtoGIDmap(string $map, int $cid, int $gid): string
|
||||
{
|
||||
// The CIDToGIDMap is a table of 16-bit big-endian values, so a glyph id outside
|
||||
// 0..0xFFFF cannot be represented; such entries are left as 0 (notdef) rather than
|
||||
// being silently wrapped into a bogus glyph id.
|
||||
if ($cid >= 0 && $cid <= 0xFFFF && $gid >= 0 && $gid <= 0xFFFF) {
|
||||
$map[$cid * 2] = \chr($gid >> 8);
|
||||
$map[($cid * 2) + 1] = \chr($gid & 0xFF);
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
+558
@@ -0,0 +1,558 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Load.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;
|
||||
|
||||
use Com\Tecnick\File\Dir;
|
||||
use Com\Tecnick\File\File as ObjFile;
|
||||
use Com\Tecnick\Pdf\Font\Exception as FontException;
|
||||
|
||||
/**
|
||||
* Com\Tecnick\Pdf\Font\Load
|
||||
*
|
||||
* @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-type TFontDataCidInfo array{
|
||||
* 'Ordering': string,
|
||||
* 'Registry': string,
|
||||
* 'Supplement': int,
|
||||
* 'uni2cid': array<int, int>,
|
||||
* }
|
||||
*
|
||||
* @phpstan-type TFontDataDesc array{
|
||||
* 'Ascent': int,
|
||||
* 'AvgWidth': int,
|
||||
* 'CapHeight': int,
|
||||
* 'Descent': int,
|
||||
* 'Flags': int,
|
||||
* 'FontBBox': string,
|
||||
* 'ItalicAngle': int,
|
||||
* 'Leading': int,
|
||||
* 'MaxWidth': int,
|
||||
* 'MissingWidth': int,
|
||||
* 'StemH': int,
|
||||
* 'StemV': int,
|
||||
* 'XHeight': int,
|
||||
* }
|
||||
*
|
||||
* @phpstan-type TFontDataEncTable array{
|
||||
* 'encodingID': int,
|
||||
* 'offset': int,
|
||||
* 'platformID': int,
|
||||
* }
|
||||
*
|
||||
* @phpstan-type TFontDataMode array{
|
||||
* 'bold': bool,
|
||||
* 'italic': bool,
|
||||
* 'linethrough': bool,
|
||||
* 'overline': bool,
|
||||
* 'underline': bool,
|
||||
* }
|
||||
*
|
||||
* @phpstan-type TFontDataTableItem array{
|
||||
* 'checkSum': int,
|
||||
* 'data': string,
|
||||
* 'length': int,
|
||||
* 'offset': int,
|
||||
* }
|
||||
*
|
||||
* @phpstan-type TFontData array{
|
||||
* 'Ascender': int,
|
||||
* 'Ascent': int,
|
||||
* 'AvgWidth': float,
|
||||
* 'CapHeight': int,
|
||||
* 'CharacterSet': string,
|
||||
* 'Descender': int,
|
||||
* 'Descent': int,
|
||||
* 'EncodingScheme': string,
|
||||
* 'FamilyName': string,
|
||||
* 'Flags': int,
|
||||
* 'FontBBox': array<int>,
|
||||
* 'FontName': string,
|
||||
* 'FullName': string,
|
||||
* 'IsFixedPitch': bool,
|
||||
* 'ItalicAngle': int,
|
||||
* 'Leading': int,
|
||||
* 'MaxWidth': int,
|
||||
* 'MissingWidth': int|null,
|
||||
* 'StdHW': int,
|
||||
* 'StdVW': int,
|
||||
* 'StemH': int,
|
||||
* 'StemV': int,
|
||||
* 'UnderlinePosition': int,
|
||||
* 'UnderlineThickness': int,
|
||||
* 'Version': string,
|
||||
* 'Weight': string,
|
||||
* 'XHeight': int,
|
||||
* 'bbox': string,
|
||||
* 'cbbox': array<int, array<int, int>>,
|
||||
* 'cidinfo': TFontDataCidInfo,
|
||||
* 'compress': bool,
|
||||
* 'ctg': string,
|
||||
* 'ctgdata': array<int, int>,
|
||||
* 'cw': array<int, int>,
|
||||
* 'cwu': array<int, int>,
|
||||
* 'datafile': string,
|
||||
* 'desc': TFontDataDesc,
|
||||
* 'diff': string,
|
||||
* 'diff_n': int,
|
||||
* 'diffid'?: int,
|
||||
* 'dir': string,
|
||||
* 'dw': int,
|
||||
* 'enc': string,
|
||||
* 'enc_map': array<int, string>,
|
||||
* 'encodingTables': array<int, TFontDataEncTable>,
|
||||
* 'encoding_id': int,
|
||||
* 'encrypted': string,
|
||||
* 'fakestyle': bool,
|
||||
* 'family': string,
|
||||
* 'file': string,
|
||||
* 'file_n': int,
|
||||
* 'file_name': string,
|
||||
* 'i': int,
|
||||
* 'ifile': string,
|
||||
* 'indexToLoc': array<int, int>,
|
||||
* 'input_file': string,
|
||||
* 'isUnicode': bool,
|
||||
* 'italicAngle': float,
|
||||
* 'key': string,
|
||||
* 'lenIV': int,
|
||||
* 'length1': int,
|
||||
* 'length2': int,
|
||||
* 'linked': bool,
|
||||
* 'mode': TFontDataMode,
|
||||
* 'n': int,
|
||||
* 'name': string,
|
||||
* 'numGlyphs': int,
|
||||
* 'numHMetrics': int,
|
||||
* 'originalsize': int,
|
||||
* 'pdfa': bool,
|
||||
* 'platform_id': int,
|
||||
* 'settype': string,
|
||||
* 'short_offset': bool,
|
||||
* 'size1': int,
|
||||
* 'size2': int,
|
||||
* 'style': string,
|
||||
* 'subset': bool,
|
||||
* 'subsetchars': array<int, bool>,
|
||||
* 'table': array<string, TFontDataTableItem>,
|
||||
* 'tot_num_glyphs': int,
|
||||
* 'type': string,
|
||||
* 'underlinePosition': int,
|
||||
* 'underlineThickness': int,
|
||||
* 'unicode': bool,
|
||||
* 'unitsPerEm': int,
|
||||
* 'up': int,
|
||||
* 'urk': float,
|
||||
* 'ut': int,
|
||||
* 'weight': string,
|
||||
* }
|
||||
*/
|
||||
abstract class Load
|
||||
{
|
||||
/**
|
||||
* File helper used to load font definition files.
|
||||
*/
|
||||
protected ObjFile $fileHelper;
|
||||
|
||||
/**
|
||||
* True when the file helper is created internally by this class.
|
||||
*/
|
||||
protected bool $ownsFileHelper = false;
|
||||
|
||||
/**
|
||||
* Valid Font types
|
||||
*
|
||||
* @var array<string, bool> Font types
|
||||
*/
|
||||
protected const FONTTYPES = [
|
||||
'Core' => true,
|
||||
'TrueType' => true,
|
||||
'TrueTypeUnicode' => true,
|
||||
'Type1' => true,
|
||||
'cidfont0' => true,
|
||||
];
|
||||
|
||||
/**
|
||||
* Font data
|
||||
*
|
||||
* @var TFontData
|
||||
*/
|
||||
protected array $data = [
|
||||
'Ascender' => 0,
|
||||
'Ascent' => 0,
|
||||
'AvgWidth' => 0.0,
|
||||
'CapHeight' => 0,
|
||||
'CharacterSet' => '',
|
||||
'Descender' => 0,
|
||||
'Descent' => 0,
|
||||
'EncodingScheme' => '',
|
||||
'FamilyName' => '',
|
||||
'Flags' => 0,
|
||||
'FontBBox' => [],
|
||||
'FontName' => '',
|
||||
'FullName' => '',
|
||||
'IsFixedPitch' => false,
|
||||
'ItalicAngle' => 0,
|
||||
'Leading' => 0,
|
||||
'MaxWidth' => 0,
|
||||
'MissingWidth' => 0,
|
||||
'StdHW' => 0,
|
||||
'StdVW' => 0,
|
||||
'StemH' => 0,
|
||||
'StemV' => 0,
|
||||
'UnderlinePosition' => 0,
|
||||
'UnderlineThickness' => 0,
|
||||
'Version' => '',
|
||||
'Weight' => '',
|
||||
'XHeight' => 0,
|
||||
'bbox' => '',
|
||||
'cbbox' => [],
|
||||
'cidinfo' => [
|
||||
'Ordering' => '',
|
||||
'Registry' => '',
|
||||
'Supplement' => 0,
|
||||
'uni2cid' => [],
|
||||
],
|
||||
'compress' => false,
|
||||
'ctg' => '',
|
||||
'ctgdata' => [],
|
||||
'cw' => [],
|
||||
'cwu' => [],
|
||||
'datafile' => '',
|
||||
'desc' => [
|
||||
'Ascent' => 0,
|
||||
'AvgWidth' => 0,
|
||||
'CapHeight' => 0,
|
||||
'Descent' => 0,
|
||||
'Flags' => 0,
|
||||
'FontBBox' => '',
|
||||
'ItalicAngle' => 0,
|
||||
'Leading' => 0,
|
||||
'MaxWidth' => 0,
|
||||
'MissingWidth' => 0,
|
||||
'StemH' => 0,
|
||||
'StemV' => 0,
|
||||
'XHeight' => 0,
|
||||
],
|
||||
'diff' => '',
|
||||
'diff_n' => 0,
|
||||
'dir' => '',
|
||||
'dw' => 0,
|
||||
'enc' => '',
|
||||
'enc_map' => [],
|
||||
'encodingTables' => [],
|
||||
'encoding_id' => 0,
|
||||
'encrypted' => '',
|
||||
'fakestyle' => false,
|
||||
'family' => '',
|
||||
'file' => '',
|
||||
'file_n' => 0,
|
||||
'file_name' => '',
|
||||
'i' => 0,
|
||||
'ifile' => '',
|
||||
'indexToLoc' => [],
|
||||
'input_file' => '',
|
||||
'isUnicode' => false,
|
||||
'italicAngle' => 0,
|
||||
'key' => '',
|
||||
'lenIV' => 0,
|
||||
'length1' => 0,
|
||||
'length2' => 0,
|
||||
'linked' => false,
|
||||
'mode' => [
|
||||
'bold' => false,
|
||||
'italic' => false,
|
||||
'linethrough' => false,
|
||||
'overline' => false,
|
||||
'underline' => false,
|
||||
],
|
||||
'n' => 0,
|
||||
'name' => '',
|
||||
'numGlyphs' => 0,
|
||||
'numHMetrics' => 0,
|
||||
'originalsize' => 0,
|
||||
'pdfa' => false,
|
||||
'platform_id' => 0,
|
||||
'settype' => '',
|
||||
'short_offset' => false,
|
||||
'size1' => 0,
|
||||
'size2' => 0,
|
||||
'style' => '',
|
||||
'subset' => false,
|
||||
'subsetchars' => [],
|
||||
'table' => [],
|
||||
'tot_num_glyphs' => 0,
|
||||
'type' => '',
|
||||
'underlinePosition' => 0,
|
||||
'underlineThickness' => 0,
|
||||
'unicode' => false,
|
||||
'unitsPerEm' => 0,
|
||||
'up' => 0,
|
||||
'urk' => 0.0,
|
||||
'ut' => 0,
|
||||
'weight' => '',
|
||||
];
|
||||
|
||||
/**
|
||||
* @param ObjFile|null $fileHelper Optional file helper for font loading.
|
||||
*/
|
||||
public function __construct(?ObjFile $fileHelper = null)
|
||||
{
|
||||
$this->ownsFileHelper = $fileHelper === null;
|
||||
$this->fileHelper = $fileHelper ?? new ObjFile(allowedPaths: $this->buildAllowedPaths());
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the font data
|
||||
*
|
||||
* @throws FontException in case of error
|
||||
*/
|
||||
public function load(): void
|
||||
{
|
||||
$this->getFontInfo();
|
||||
$this->checkType();
|
||||
$this->setName();
|
||||
$this->setDefaultWidth();
|
||||
if ($this->data['fakestyle']) {
|
||||
$this->setArtificialStyles();
|
||||
}
|
||||
|
||||
$this->setFileData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the font data
|
||||
*
|
||||
* @throws FontException in case of error
|
||||
*
|
||||
* @SuppressWarnings("PHPMD.CyclomaticComplexity")
|
||||
*/
|
||||
protected function getFontInfo(): void
|
||||
{
|
||||
$this->findFontFile();
|
||||
|
||||
if ($this->ownsFileHelper) {
|
||||
$this->fileHelper->setAllowedPaths($this->buildAllowedPaths());
|
||||
}
|
||||
|
||||
// read the font definition file
|
||||
$fdt = $this->fileHelper->getFileData($this->data['ifile']);
|
||||
if ($fdt === false) {
|
||||
throw new FontException('unable to read file: ' . $this->data['ifile']);
|
||||
}
|
||||
|
||||
/** @var array<string, mixed>|null $fdtdata */
|
||||
$fdtdata = \json_decode($fdt, true, 5, JSON_OBJECT_AS_ARRAY);
|
||||
if ($fdtdata === null) {
|
||||
throw new FontException('JSON decoding error [' . \json_last_error() . ']');
|
||||
}
|
||||
|
||||
if (!isset($fdtdata['type'])) {
|
||||
throw new FontException('The font definition file has a bad format: ' . $this->data['ifile']);
|
||||
}
|
||||
|
||||
$merged = \array_replace_recursive($this->data, $fdtdata);
|
||||
/** @var TFontData $merged */
|
||||
$this->data = $merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of font directories
|
||||
*
|
||||
* @return array<string> Font directories
|
||||
*/
|
||||
protected function findFontDirectories(): array
|
||||
{
|
||||
$dir = new Dir();
|
||||
$dirs = [];
|
||||
if (\defined('K_PATH_FONTS')) {
|
||||
$kpathfonts = (string) \constant('K_PATH_FONTS');
|
||||
if ($kpathfonts !== '') {
|
||||
$dirs[] = $kpathfonts;
|
||||
$glb = \glob($kpathfonts . DIRECTORY_SEPARATOR . '*', GLOB_ONLYDIR);
|
||||
if ($glb !== false) {
|
||||
$dirs = [...$dirs, ...$glb];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$parent_font_dir = $dir->findParentDir('fonts', __DIR__);
|
||||
if ($parent_font_dir !== '' && $parent_font_dir !== '/') {
|
||||
$dirs[] = $parent_font_dir;
|
||||
$glb = \glob($parent_font_dir . DIRECTORY_SEPARATOR . '*', GLOB_ONLYDIR);
|
||||
if ($glb !== false) {
|
||||
$dirs = \array_merge($dirs, $glb);
|
||||
}
|
||||
}
|
||||
|
||||
return \array_unique($dirs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build trusted roots for local font definition loading.
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
protected function buildAllowedPaths(): array
|
||||
{
|
||||
return FontPaths::buildAllowedPaths();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the font data
|
||||
*
|
||||
* @throws FontException in case of error
|
||||
*/
|
||||
protected function findFontFile(): void
|
||||
{
|
||||
if ($this->data['ifile'] !== '') {
|
||||
$this->data['dir'] = \dirname($this->data['ifile']);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->data['ifile'] = \strtolower($this->data['key']) . '.json';
|
||||
|
||||
// find font definition file names
|
||||
$files = \array_unique([
|
||||
\strtolower($this->data['key']) . '.json',
|
||||
\strtolower($this->data['family']) . '.json',
|
||||
]);
|
||||
|
||||
// directories where to search for the font definition file
|
||||
$dirs = $this->findFontDirectories();
|
||||
|
||||
foreach ($files as $file) {
|
||||
foreach ($dirs as $dir) {
|
||||
if (!\is_readable($dir . DIRECTORY_SEPARATOR . $file)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->data['ifile'] = $dir . DIRECTORY_SEPARATOR . $file;
|
||||
$this->data['dir'] = $dir;
|
||||
break 2;
|
||||
}
|
||||
|
||||
// we have not found the version with style variations
|
||||
$this->data['fakestyle'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected function setDefaultWidth(): void
|
||||
{
|
||||
if ($this->data['dw'] !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->data['desc']['MissingWidth'] > 0) {
|
||||
$this->data['dw'] = $this->data['desc']['MissingWidth'];
|
||||
} elseif (isset($this->data['cw'][32]) && $this->data['cw'][32] !== 0) {
|
||||
$this->data['dw'] = $this->data['cw'][32];
|
||||
} else {
|
||||
$this->data['dw'] = 600;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check Font Type
|
||||
*
|
||||
* @throws FontException on unknown font type
|
||||
*/
|
||||
protected function checkType(): void
|
||||
{
|
||||
if (isset(self::FONTTYPES[$this->data['type']])) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new FontException('Unknown font type: ' . $this->data['type']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*
|
||||
* @throws FontException on using a CID0 font in a pdfa
|
||||
*/
|
||||
protected function setName(): void
|
||||
{
|
||||
if ($this->data['type'] === 'Core') {
|
||||
$this->data['name'] = Core::FONT[$this->data['key']] ?? $this->data['key'];
|
||||
$this->data['subset'] = false;
|
||||
} elseif ($this->data['type'] === 'Type1' || $this->data['type'] === 'TrueType') {
|
||||
$this->data['subset'] = false;
|
||||
} elseif ($this->data['type'] === 'TrueTypeUnicode') {
|
||||
$this->data['enc'] = 'Identity-H';
|
||||
} elseif ($this->data['type'] === 'cidfont0' && $this->data['pdfa']) {
|
||||
throw new FontException('CID0 fonts are not supported, all fonts must be embedded in PDF/A mode!');
|
||||
}
|
||||
|
||||
if ($this->data['name'] === '') {
|
||||
$this->data['name'] = $this->data['key'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set artificial styles if the font variation file is missing
|
||||
*/
|
||||
protected function setArtificialStyles(): void
|
||||
{
|
||||
// artificial bold
|
||||
if ($this->data['mode']['bold']) {
|
||||
$this->data['name'] .= 'Bold';
|
||||
$this->data['desc']['StemV'] = $this->data['desc']['StemV'] === 0
|
||||
? 123
|
||||
: (int) \round($this->data['desc']['StemV'] * 1.75);
|
||||
}
|
||||
|
||||
// artificial italic
|
||||
if ($this->data['mode']['italic']) {
|
||||
$this->data['name'] .= 'Italic';
|
||||
if ($this->data['desc']['ItalicAngle'] !== 0) {
|
||||
$this->data['desc']['ItalicAngle'] -= 11;
|
||||
} else {
|
||||
$this->data['desc']['ItalicAngle'] = -11;
|
||||
}
|
||||
|
||||
if ($this->data['desc']['Flags'] !== 0) {
|
||||
$this->data['desc']['Flags'] |= 64; //bit 7
|
||||
} else {
|
||||
$this->data['desc']['Flags'] = 64;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function setFileData(): void
|
||||
{
|
||||
if ($this->data['file'] === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (\str_contains($this->data['type'], 'TrueType')) {
|
||||
$this->data['length1'] = $this->data['originalsize'];
|
||||
$this->data['length2'] = 0;
|
||||
} elseif ($this->data['type'] !== 'Core') {
|
||||
$this->data['length1'] = $this->data['size1'];
|
||||
$this->data['length2'] = $this->data['size2'];
|
||||
}
|
||||
}
|
||||
}
|
||||
+467
@@ -0,0 +1,467 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* OutFont.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;
|
||||
|
||||
use Com\Tecnick\File\File as ObjFile;
|
||||
use Com\Tecnick\Pdf\Encrypt\Encrypt;
|
||||
use Com\Tecnick\Pdf\Encrypt\Exception as EncException;
|
||||
use Com\Tecnick\Pdf\Font\Exception as FontException;
|
||||
use Com\Tecnick\Unicode\Data\Identity;
|
||||
|
||||
/**
|
||||
* Com\Tecnick\Pdf\Font\OutFont
|
||||
*
|
||||
* @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 Load
|
||||
* @phpstan-import-type TFontDataCidInfo from Load
|
||||
* @phpstan-import-type TFontDataDesc from Load
|
||||
*/
|
||||
abstract class OutFont extends \Com\Tecnick\Pdf\Font\OutUtil
|
||||
{
|
||||
/**
|
||||
* Current PDF object number
|
||||
*/
|
||||
protected int $pon;
|
||||
|
||||
/**
|
||||
* Encrypt object
|
||||
*/
|
||||
protected Encrypt $enc;
|
||||
|
||||
/**
|
||||
* File helper used to load font files.
|
||||
*/
|
||||
protected ObjFile $fileHelper;
|
||||
|
||||
/**
|
||||
* Get the PDF output string for a CID-0 font.
|
||||
* A Type 0 CIDFont contains glyph descriptions based on the Adobe Type 1 font format
|
||||
*
|
||||
* @param TFontData $font Font to process
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @throws EncException
|
||||
*/
|
||||
protected function getCid0(array $font): string
|
||||
{
|
||||
$fontcw = $font['cw'];
|
||||
$fontname = $font['name'];
|
||||
$fontenc = $font['enc'];
|
||||
$fontn = $font['n'];
|
||||
$fonti = $font['i'];
|
||||
$fontdw = $font['dw'];
|
||||
$fontdesc = $font['desc'];
|
||||
$fontcidinfo = $font['cidinfo'];
|
||||
$cidregistry = $fontcidinfo['Registry'];
|
||||
$cidordering = $fontcidinfo['Ordering'];
|
||||
$cidsupplement = $fontcidinfo['Supplement'];
|
||||
|
||||
$cidoffset = 0;
|
||||
if (!isset($fontcw[1])) {
|
||||
$cidoffset = 31;
|
||||
}
|
||||
|
||||
$this->uniToCid($font, $cidoffset);
|
||||
$name = $fontname;
|
||||
$longname = $name;
|
||||
if ($fontenc !== '') {
|
||||
$longname .= '-' . $fontenc;
|
||||
}
|
||||
|
||||
// obj 1
|
||||
$out =
|
||||
$fontn
|
||||
. ' 0 obj'
|
||||
. "\n"
|
||||
. '<</Type /Font'
|
||||
. ' /Subtype /Type0'
|
||||
. ' /BaseFont /'
|
||||
. $longname
|
||||
. ' /Name /F'
|
||||
. $fonti;
|
||||
if ($fontenc !== '') {
|
||||
$out .= ' /Encoding /' . $fontenc;
|
||||
}
|
||||
|
||||
$out .= ' /DescendantFonts [' . ($this->pon + 1) . ' 0 R] >>' . "\n" . 'endobj' . "\n";
|
||||
|
||||
// obj 2
|
||||
$out .= ++$this->pon . ' 0 obj' . "\n" . '<</Type /Font /Subtype /CIDFontType0 /BaseFont /' . $name;
|
||||
$cidinfo =
|
||||
'/Registry '
|
||||
. $this->enc->escapeDataString($cidregistry, $this->pon)
|
||||
. ' /Ordering '
|
||||
. $this->enc->escapeDataString($cidordering, $this->pon)
|
||||
. ' /Supplement '
|
||||
. $cidsupplement;
|
||||
$out .=
|
||||
' /CIDSystemInfo <<'
|
||||
. $cidinfo
|
||||
. '>>'
|
||||
. ' /FontDescriptor '
|
||||
. ($this->pon + 1)
|
||||
. ' 0 R'
|
||||
. ' /DW '
|
||||
. $fontdw
|
||||
. "\n"
|
||||
. $this->getCharWidths($font, $cidoffset)
|
||||
. ' >>'
|
||||
. "\n"
|
||||
. 'endobj'
|
||||
. "\n";
|
||||
|
||||
// obj 3
|
||||
$out .= ++$this->pon . ' 0 obj' . "\n" . '<</Type /FontDescriptor /FontName /' . $name;
|
||||
foreach ($fontdesc as $key => $val) {
|
||||
$out .= $this->getKeyValOut($key, $val);
|
||||
}
|
||||
|
||||
return $out . ('>>' . "\n" . 'endobj' . "\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Unicode to CID
|
||||
*
|
||||
* @param TFontData $font Font to process
|
||||
* @param int $cidoffset Offset for CID values
|
||||
*/
|
||||
protected function uniToCid(array &$font, int $cidoffset): void
|
||||
{
|
||||
// convert unicode to cid.
|
||||
$fontcidinfo = $font['cidinfo'];
|
||||
$uni2cidraw = $fontcidinfo['uni2cid'];
|
||||
$uni2cid = [];
|
||||
foreach ($uni2cidraw as $uni => $cid) {
|
||||
$uni2cid[(int) $uni] = (int) $cid;
|
||||
}
|
||||
|
||||
$fontcwraw = $font['cw'];
|
||||
$fontcw = [];
|
||||
foreach ($fontcwraw as $uni => $width) {
|
||||
$fontcw[(int) $uni] = (int) $width;
|
||||
}
|
||||
|
||||
$chw = [];
|
||||
foreach ($fontcw as $uni => $width) {
|
||||
if (isset($uni2cid[$uni])) {
|
||||
$chw[$uni2cid[$uni] + $cidoffset] = $width;
|
||||
} elseif ($uni < 256) {
|
||||
$chw[$uni] = $width;
|
||||
} // else unknown character
|
||||
}
|
||||
|
||||
foreach ($chw as $cid => $width) {
|
||||
$fontcw[$cid] = $width;
|
||||
}
|
||||
|
||||
$font['cw'] = $fontcw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the PDF output string for a TrueTypeUnicode font.
|
||||
* Based on PDF Reference 1.3 (section 5)
|
||||
*
|
||||
* @param TFontData $font Font to process
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @throws EncException
|
||||
* @throws FontException
|
||||
* @throws \RuntimeException
|
||||
*
|
||||
* @SuppressWarnings("PHPMD.ExcessiveMethodLength")
|
||||
* @SuppressWarnings("PHPMD.CyclomaticComplexity")
|
||||
* @SuppressWarnings("PHPMD.NPathComplexity")
|
||||
*/
|
||||
protected function getTrueTypeUnicode(array $font): string
|
||||
{
|
||||
$fontsubset = $font['subset'];
|
||||
$fonti = $font['i'];
|
||||
$fontn = $font['n'];
|
||||
$fontenc = $font['enc'];
|
||||
$fontnamebase = $font['name'];
|
||||
$fontdw = $font['dw'];
|
||||
$fontctg = $font['ctg'];
|
||||
$fontdir = $font['dir'];
|
||||
$fontfilen = $font['file_n'];
|
||||
$fontdesc = $font['desc'];
|
||||
$fontcidinfo = $font['cidinfo'];
|
||||
|
||||
$fontname = '';
|
||||
if ($fontsubset) {
|
||||
// change name for font subsetting
|
||||
$subtag = \sprintf('%06u', $fonti);
|
||||
$subtag = \strtr($subtag, '0123456789', 'ABCDEFGHIJ');
|
||||
$fontname .= $subtag . '+';
|
||||
}
|
||||
|
||||
$fontname .= $fontnamebase;
|
||||
|
||||
// Type0 Font
|
||||
// A composite font composed of other fonts, organized hierarchically
|
||||
|
||||
// obj 1
|
||||
$out =
|
||||
$fontn
|
||||
. ' 0 obj'
|
||||
. "\n"
|
||||
. '<< /Type /Font'
|
||||
. ' /Subtype /Type0'
|
||||
. ' /BaseFont /'
|
||||
. $fontname
|
||||
. ' /Name /F'
|
||||
. $fonti
|
||||
. ' /Encoding /'
|
||||
. $fontenc
|
||||
. ' /ToUnicode '
|
||||
. ($this->pon + 1)
|
||||
. ' 0 R'
|
||||
. ' /DescendantFonts ['
|
||||
. ($this->pon + 2)
|
||||
. ' 0 R]'
|
||||
. ' >>'
|
||||
. "\n"
|
||||
. 'endobj'
|
||||
. "\n";
|
||||
|
||||
// ToUnicode Object
|
||||
$out .= ++$this->pon . ' 0 obj' . "\n" . '<<';
|
||||
$cidhmap = Identity::CIDHMAP;
|
||||
if ($font['compress']) {
|
||||
$out .= ' /Filter /FlateDecode';
|
||||
$cidhmap = \gzcompress($cidhmap);
|
||||
if ($cidhmap === false) {
|
||||
throw new \RuntimeException('Unable to compress CIDHMAP');
|
||||
}
|
||||
}
|
||||
|
||||
$stream = $this->enc->encryptString($cidhmap, $this->pon); // ToUnicode map for Identity-H
|
||||
$out .=
|
||||
' /Length '
|
||||
. \strlen($stream)
|
||||
. ' >>'
|
||||
. ' stream'
|
||||
. "\n"
|
||||
. $stream
|
||||
. "\n"
|
||||
. 'endstream'
|
||||
. "\n"
|
||||
. 'endobj'
|
||||
. "\n";
|
||||
|
||||
// CIDFontType2
|
||||
// A CIDFont whose glyph descriptions are based on TrueType font technology
|
||||
$out .= ++$this->pon . ' 0 obj' . "\n" . '<< /Type /Font /Subtype /CIDFontType2 /BaseFont /' . $fontname;
|
||||
// A dictionary containing entries that define the character collection of the CIDFont.
|
||||
$cidRegistry = $fontcidinfo['Registry'] === '' ? 'Adobe' : $fontcidinfo['Registry'];
|
||||
$cidOrdering = $fontcidinfo['Ordering'] === '' ? 'Identity' : $fontcidinfo['Ordering'];
|
||||
$cidinfo =
|
||||
'/Registry '
|
||||
. $this->enc->escapeDataString($cidRegistry, $this->pon)
|
||||
. ' /Ordering '
|
||||
. $this->enc->escapeDataString($cidOrdering, $this->pon)
|
||||
. ' /Supplement '
|
||||
. $fontcidinfo['Supplement'];
|
||||
$out .=
|
||||
' /CIDSystemInfo << '
|
||||
. $cidinfo
|
||||
. ' >>'
|
||||
. ' /FontDescriptor '
|
||||
. ($this->pon + 1)
|
||||
. ' 0 R'
|
||||
. ' /DW '
|
||||
. $fontdw
|
||||
. "\n"
|
||||
. $this->getCharWidths($font, 0);
|
||||
if ($fontctg !== '') {
|
||||
$out .= "\n" . '/CIDToGIDMap ' . ($this->pon + 2) . ' 0 R';
|
||||
}
|
||||
|
||||
$out .= ' >>' . "\n" . 'endobj' . "\n";
|
||||
|
||||
// Font descriptor
|
||||
// A font descriptor describing the CIDFont default metrics other than its glyph widths
|
||||
$out .= ++$this->pon . ' 0 obj' . "\n" . '<< /Type /FontDescriptor /FontName /' . $fontname;
|
||||
foreach ($fontdesc as $key => $val) {
|
||||
$out .= $this->getKeyValOut($key, $val);
|
||||
}
|
||||
|
||||
if ($fontfilen > 0) {
|
||||
// A stream containing a TrueType font
|
||||
$out .= ' /FontFile2 ' . $fontfilen . ' 0 R';
|
||||
}
|
||||
|
||||
$out .= ' >>' . "\n" . 'endobj' . "\n";
|
||||
|
||||
if ($fontctg !== '') {
|
||||
$out .= ++$this->pon . ' 0 obj' . "\n";
|
||||
// Embed CIDToGIDMap
|
||||
// A specification of the mapping from CIDs to glyph indices
|
||||
// search and get CTG font file to embed
|
||||
$ctgfile = \strtolower($fontctg);
|
||||
// search and get ctg font file to embed
|
||||
$fontfile = $this->getFontFullPath($fontdir, $ctgfile);
|
||||
$content = $this->fileHelper->getLocalFileData($fontfile);
|
||||
if ($content === false) {
|
||||
throw new FontException('Unable to read font file: ' . $fontfile);
|
||||
}
|
||||
|
||||
$stream = $this->enc->encryptString($content, $this->pon);
|
||||
$out .= '<< /Length ' . \strlen($stream) . '';
|
||||
if (\str_ends_with($fontfile, '.z')) { // check file extension
|
||||
// Decompresses data encoded using the public-domain
|
||||
// zlib/deflate compression method, reproducing the
|
||||
// original text or binary data
|
||||
$out .= ' /Filter /FlateDecode';
|
||||
}
|
||||
|
||||
$out .= ' >> stream' . "\n" . $stream . "\n" . 'endstream' . "\n" . 'endobj' . "\n";
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the PDF output string for a Core font.
|
||||
*
|
||||
* @param TFontData $font Font to process
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getCore(array $font): string
|
||||
{
|
||||
$fontn = $font['n'];
|
||||
$fontname = $font['name'];
|
||||
$fonti = $font['i'];
|
||||
$fontfamily = $font['family'];
|
||||
|
||||
$out =
|
||||
$fontn
|
||||
. ' 0 obj'
|
||||
. "\n"
|
||||
. '<</Type /Font'
|
||||
. ' /Subtype /Type1'
|
||||
. ' /BaseFont /'
|
||||
. $fontname
|
||||
. ' /Name /F'
|
||||
. $fonti;
|
||||
if ($fontfamily !== 'symbol' && $fontfamily !== 'zapfdingbats') {
|
||||
$out .= ' /Encoding /WinAnsiEncoding';
|
||||
}
|
||||
|
||||
return $out . (' >>' . "\n" . 'endobj' . "\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the PDF output string for a TrueType font.
|
||||
*
|
||||
* @param TFontData $font Font to process
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getTrueType(array $font): string
|
||||
{
|
||||
$fontname = $font['name'];
|
||||
$fonttype = $font['type'];
|
||||
$fonti = $font['i'];
|
||||
$fontn = $font['n'];
|
||||
$fontdw = $font['dw'];
|
||||
$fontfile = $font['file'];
|
||||
$fontfilen = $font['file_n'];
|
||||
$fontenc = $font['enc'];
|
||||
$fontdesc = $font['desc'];
|
||||
$fontcw = $font['cw'];
|
||||
|
||||
// obj 1
|
||||
$out =
|
||||
$fontn
|
||||
. ' 0 obj'
|
||||
. "\n"
|
||||
. '<</Type /Font'
|
||||
. ' /Subtype /'
|
||||
. $fonttype
|
||||
. ' /BaseFont /'
|
||||
. $fontname
|
||||
. ' /Name /F'
|
||||
. $fonti
|
||||
. ' /FirstChar 32 /LastChar 255'
|
||||
. ' /Widths '
|
||||
. ($this->pon + 1)
|
||||
. ' 0 R'
|
||||
. ' /FontDescriptor '
|
||||
. ($this->pon + 2)
|
||||
. ' 0 R';
|
||||
if ($fontenc !== '') {
|
||||
if ($font['diff_n'] !== 0) {
|
||||
$out .= ' /Encoding ' . $font['diff_n'] . ' 0 R';
|
||||
} else {
|
||||
$out .= ' /Encoding /WinAnsiEncoding';
|
||||
}
|
||||
}
|
||||
|
||||
$out .= ' >>' . "\n" . 'endobj' . "\n";
|
||||
|
||||
// obj 2 - Widths
|
||||
$out .= ++$this->pon . ' 0 obj' . "\n" . '[';
|
||||
for ($idx = 32; $idx < 256; ++$idx) {
|
||||
if (isset($fontcw[$idx])) {
|
||||
$out .= (int) $fontcw[$idx] . ' ';
|
||||
} else {
|
||||
$out .= $fontdw . ' ';
|
||||
}
|
||||
}
|
||||
|
||||
$out .= ']' . "\n" . 'endobj' . "\n";
|
||||
|
||||
// obj 3 - Descriptor
|
||||
$out .= ++$this->pon . ' 0 obj' . "\n" . '<</Type /FontDescriptor /FontName /' . $fontname;
|
||||
foreach ($fontdesc as $fdk => $fdv) {
|
||||
$out .= $this->getKeyValOut($fdk, $fdv);
|
||||
}
|
||||
|
||||
if ($fontfile !== '') {
|
||||
$out .= ' /FontFile' . ($fonttype === 'Type1' ? '' : '2') . ' ' . $fontfilen . ' 0 R';
|
||||
}
|
||||
|
||||
return $out . ('>>' . "\n" . 'endobj' . "\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the formatted key/value PDF string
|
||||
*
|
||||
* @param string $key Key name
|
||||
* @param mixed $val Value
|
||||
*/
|
||||
protected function getKeyValOut(string $key, mixed $val): string
|
||||
{
|
||||
if (\is_float($val)) {
|
||||
$val = \sprintf('%F', $val);
|
||||
}
|
||||
|
||||
return ' /' . $key . ' ' . (string) $val;
|
||||
}
|
||||
}
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* OutUtil.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;
|
||||
|
||||
use Com\Tecnick\File\Dir;
|
||||
use Com\Tecnick\Pdf\Font\Exception as FontException;
|
||||
|
||||
/**
|
||||
* Com\Tecnick\Pdf\Font\OutUtil
|
||||
*
|
||||
* @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 Load
|
||||
*/
|
||||
abstract class OutUtil
|
||||
{
|
||||
/**
|
||||
* Return font full path
|
||||
*
|
||||
* @param string $fontdir Original font directory
|
||||
* @param string $file Font file name.
|
||||
*
|
||||
* @return string Font full path or empty string
|
||||
*
|
||||
* @throws FontException
|
||||
*/
|
||||
protected function getFontFullPath(string $fontdir, string $file): string
|
||||
{
|
||||
$dirobj = new Dir();
|
||||
$kpathfonts = \defined('K_PATH_FONTS') ? (string) \constant('K_PATH_FONTS') : '';
|
||||
// directories where to search for the font definition file
|
||||
// ('.' searches the current working directory)
|
||||
$dirs = \array_unique([
|
||||
'.',
|
||||
$fontdir,
|
||||
$kpathfonts,
|
||||
$dirobj->findParentDir('fonts', __DIR__),
|
||||
]);
|
||||
foreach ($dirs as $dir) {
|
||||
if (\is_readable($dir . DIRECTORY_SEPARATOR . $file)) {
|
||||
return $dir . DIRECTORY_SEPARATOR . $file;
|
||||
}
|
||||
}
|
||||
|
||||
throw new FontException('Unable to locate the file: ' . $file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Outputs font widths
|
||||
*
|
||||
* @param TFontData $font Font to process
|
||||
* @param int $cidoffset Offset for CID values
|
||||
*
|
||||
* @return string PDF command string for font widths
|
||||
*/
|
||||
protected function getCharWidths(array $font, int $cidoffset = 0): string
|
||||
{
|
||||
\ksort($font['cw']);
|
||||
$range = $this->getWidthRanges($font, $cidoffset);
|
||||
// output data
|
||||
$wdt = '';
|
||||
foreach ($range as $kdx => $wds) {
|
||||
if (\count(\array_count_values($wds)) === 1) {
|
||||
// interval mode is more compact
|
||||
$wdt .= ' ' . $kdx . ' ' . ($kdx + \count($wds) - 1) . ' ' . $wds[0];
|
||||
} else {
|
||||
// range mode
|
||||
$wdt .= ' ' . $kdx . ' [ ' . \implode(' ', $wds) . ' ]';
|
||||
}
|
||||
}
|
||||
|
||||
return '/W [' . $wdt . ' ]';
|
||||
}
|
||||
|
||||
/**
|
||||
* get width ranges of characters
|
||||
*
|
||||
* @param TFontData $font Font to process
|
||||
* @param int $cidoffset Offset for CID values
|
||||
*
|
||||
* @return array<int, array<int, int>>
|
||||
*/
|
||||
protected function getWidthRanges(array $font, int $cidoffset = 0): array
|
||||
{
|
||||
$range = [];
|
||||
$rangeid = 0;
|
||||
$prevcid = -2;
|
||||
$prevwidth = -1;
|
||||
$interval = false;
|
||||
// for each character
|
||||
foreach ($font['cw'] as $cid => $width) {
|
||||
$cid -= $cidoffset;
|
||||
if ($font['subset'] && !isset($font['subsetchars'][$cid])) {
|
||||
// ignore the unused characters (font subsetting)
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($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][-1] = -1;
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
/** @var array<int, array<int, int>> $range */
|
||||
return $this->optimizeWidthRanges($range);
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimize width ranges
|
||||
*
|
||||
* @param array<int, array<int, int>> $range Width Ranges
|
||||
*
|
||||
* @return array<int, array<int, int>>
|
||||
*/
|
||||
protected function optimizeWidthRanges(array $range): array
|
||||
{
|
||||
$prevk = -1;
|
||||
$nextk = -1;
|
||||
$prevint = false;
|
||||
foreach ($range as $kdx => $wds) {
|
||||
$cws = \count($wds);
|
||||
if ($kdx === $nextk && !$prevint && (!isset($wds[-1]) || $cws < 4)) {
|
||||
unset($range[$kdx][-1]);
|
||||
$range[$prevk] = [...$range[$prevk], ...$range[$kdx]];
|
||||
unset($range[$kdx]);
|
||||
} else {
|
||||
$prevk = $kdx;
|
||||
}
|
||||
|
||||
$prevint = false;
|
||||
$nextk = $kdx + $cws;
|
||||
if (isset($wds[-1])) {
|
||||
unset($range[$kdx][-1]);
|
||||
$prevint = $cws > 3;
|
||||
--$nextk;
|
||||
}
|
||||
}
|
||||
|
||||
return $range;
|
||||
}
|
||||
}
|
||||
+362
@@ -0,0 +1,362 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Output.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;
|
||||
|
||||
use Com\Tecnick\File\Exception as FileException;
|
||||
use Com\Tecnick\File\File as ObjFile;
|
||||
use Com\Tecnick\Pdf\Encrypt\Encrypt;
|
||||
use Com\Tecnick\Pdf\Font\Exception as FontException;
|
||||
|
||||
/**
|
||||
* Com\Tecnick\Pdf\Font\Output
|
||||
*
|
||||
* @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 Load
|
||||
*/
|
||||
class Output extends \Com\Tecnick\Pdf\Font\OutFont
|
||||
{
|
||||
/**
|
||||
* Namespace and schema-version prefix for subset cache keys.
|
||||
*
|
||||
* Bump the trailing version segment to invalidate previously cached
|
||||
* subsets whenever the subsetting algorithm or key format changes.
|
||||
*/
|
||||
protected const SUBSET_CACHE_KEY_PREFIX = 'tc-lib-pdf-font:subset:v2:';
|
||||
|
||||
/**
|
||||
* Array of character subsets for each font file
|
||||
*
|
||||
* @var array<string, array<int, bool>>
|
||||
*/
|
||||
protected array $subchars = [];
|
||||
|
||||
/**
|
||||
* PDF string block with the fonts definitions
|
||||
*/
|
||||
protected string $out = '';
|
||||
|
||||
/**
|
||||
* Initialize font data
|
||||
*
|
||||
* @param array<string, TFontData> $fonts Array of imported fonts data
|
||||
* @param int $pon Current PDF Object Number
|
||||
* @param Encrypt $encrypt Encrypt object
|
||||
* @param ObjFile $fileHelper File helper for font loading.
|
||||
* @param FontSubsetCacheInterface $subsetCache Optional cache for subset font programs.
|
||||
*
|
||||
* @throws FileException
|
||||
* @throws FontException
|
||||
*/
|
||||
public function __construct(
|
||||
protected array $fonts,
|
||||
int $pon,
|
||||
Encrypt $encrypt,
|
||||
?ObjFile $fileHelper = null,
|
||||
protected ?FontSubsetCacheInterface $subsetCache = null,
|
||||
) {
|
||||
$this->fileHelper = $fileHelper ?? new ObjFile(allowedPaths: $this->buildAllowedPaths());
|
||||
|
||||
$this->pon = $pon;
|
||||
$this->enc = $encrypt;
|
||||
|
||||
$this->out = $this->getEncodingDiffs();
|
||||
$this->out .= $this->getFontFiles();
|
||||
$this->out .= $this->getFontDefinitions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build trusted roots for local font file loading.
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
private function buildAllowedPaths(): array
|
||||
{
|
||||
return FontPaths::buildAllowedPaths();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns current PDF object number
|
||||
*/
|
||||
public function getObjectNumber(): int
|
||||
{
|
||||
return $this->pon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the PDF fonts block
|
||||
*/
|
||||
public function getFontsBlock(): string
|
||||
{
|
||||
return $this->out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the PDF output string for Font resources dictionary.
|
||||
*
|
||||
* @param array<string, TFontData|array{'i': int, 'n': int}> $data Font data.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getOutFontResources(array $data): string
|
||||
{
|
||||
if ($data === []) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$out = ' /Font <<';
|
||||
|
||||
foreach ($data as $font) {
|
||||
$out .= ' /F' . (int) $font['i'] . ' ' . (int) $font['n'] . ' 0 R';
|
||||
}
|
||||
|
||||
return $out . ' >>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the PDF output string for Font resources dictionary.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getOutFontDict(): string
|
||||
{
|
||||
return $this->getOutFontResources($this->fonts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the PDF output string for XOBject Font resources dictionary.
|
||||
*
|
||||
* @param array<string> $keys Array of font keys.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getOutFontDictByKeys(array $keys): string
|
||||
{
|
||||
if ($keys === []) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$data = [];
|
||||
foreach ($keys as $key) {
|
||||
$data[$key] = [
|
||||
'i' => $this->fonts[$key]['i'],
|
||||
'n' => $this->fonts[$key]['n'],
|
||||
];
|
||||
}
|
||||
|
||||
return $this->getOutFontResources($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the PDF output string for font encoding diffs
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getEncodingDiffs(): string
|
||||
{
|
||||
$out = '';
|
||||
$done = []; // store processed items to avoid duplication
|
||||
foreach ($this->fonts as $fkey => $font) {
|
||||
if ($font['diff'] !== '') {
|
||||
$dkey = \md5($font['diff']);
|
||||
if (!isset($done[$dkey])) {
|
||||
$out .=
|
||||
++$this->pon
|
||||
. ' 0 obj'
|
||||
. "\n"
|
||||
. '<< /Type /Encoding /BaseEncoding /WinAnsiEncoding /Differences ['
|
||||
. $font['diff']
|
||||
. '] >>'
|
||||
. "\n"
|
||||
. 'endobj'
|
||||
. "\n";
|
||||
$done[$dkey] = $this->pon;
|
||||
}
|
||||
|
||||
$this->fonts[$fkey]['diff_n'] = $done[$dkey];
|
||||
}
|
||||
|
||||
// extract the character subset
|
||||
if ($font['file'] !== '') {
|
||||
$file_key = \md5($font['file']);
|
||||
if (!isset($this->subchars[$file_key]) || $this->subchars[$file_key] === []) {
|
||||
$this->subchars[$file_key] = $font['subsetchars'];
|
||||
} else {
|
||||
foreach ($font['subsetchars'] as $cid => $enabled) {
|
||||
if (!$enabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->subchars[$file_key][(int) $cid] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the PDF output string for font files
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @throws FileException
|
||||
* @throws FontException
|
||||
*/
|
||||
protected function getFontFiles(): string
|
||||
{
|
||||
$out = '';
|
||||
$done = []; // store processed items to avoid duplication
|
||||
foreach ($this->fonts as $fkey => $font) {
|
||||
if ($font['file'] === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$dkey = \md5($font['file']);
|
||||
if (!isset($done[$dkey])) {
|
||||
$fontfile = $this->getFontFullPath($font['dir'], $font['file']);
|
||||
$font_data = $this->fileHelper->getLocalFileData($fontfile);
|
||||
if ($font_data === false) {
|
||||
throw new FontException('Unable to read font file: ' . $fontfile);
|
||||
}
|
||||
|
||||
if ($font['subset']) {
|
||||
$font_data = \gzuncompress($font_data);
|
||||
if ($font_data === false) {
|
||||
throw new FontException('Unable to uncompress font file: ' . $fontfile);
|
||||
}
|
||||
|
||||
$subchars = $this->subchars[$dkey];
|
||||
// Only derive the cache key when a cache is configured: subsetCacheKey()
|
||||
// hashes the whole (multi-MB) font program, which is pure waste otherwise.
|
||||
$cache = $this->subsetCache;
|
||||
$cacheKey = '';
|
||||
$subsetFont = null;
|
||||
if ($cache !== null) {
|
||||
$cacheKey = $this->subsetCacheKey($font_data, $font, $subchars);
|
||||
$subsetFont = $cache->get($cacheKey);
|
||||
}
|
||||
|
||||
if ($subsetFont === null) {
|
||||
$sub = new Subset($font_data, $font, $this->fileHelper, $subchars);
|
||||
$subsetFont = $sub->getSubsetFont();
|
||||
$cache?->set($cacheKey, $subsetFont);
|
||||
}
|
||||
|
||||
$font_data = $subsetFont;
|
||||
$font['length1'] = \strlen($font_data);
|
||||
$font_data = \gzcompress($font_data);
|
||||
if ($font_data === false) {
|
||||
throw new FontException('Unable to compress font file: ' . $fontfile);
|
||||
}
|
||||
}
|
||||
|
||||
++$this->pon;
|
||||
$stream = $this->enc->encryptString($font_data, $this->pon);
|
||||
$out .=
|
||||
$this->pon
|
||||
. ' 0 obj'
|
||||
. "\n"
|
||||
. '<<'
|
||||
. ' /Filter /FlateDecode'
|
||||
. ' /Length '
|
||||
. \strlen($stream)
|
||||
. ' /Length1 '
|
||||
. $font['length1'];
|
||||
if ($font['type'] === 'Type1') {
|
||||
// Length2/Length3 are only valid for Type1 FontFile streams,
|
||||
// not for TrueType (FontFile2) or CFF (FontFile3) programs.
|
||||
$out .= ' /Length2 ' . $font['length2'] . ' /Length3 0';
|
||||
}
|
||||
|
||||
$out .= ' >> stream' . "\n" . $stream . "\n" . 'endstream' . "\n" . 'endobj' . "\n";
|
||||
$done[$dkey] = $this->pon;
|
||||
}
|
||||
|
||||
$this->fonts[$fkey]['file_n'] = $done[$dkey];
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the cache key identifying a subset font program.
|
||||
*
|
||||
* The subset output is fully determined by the uncompressed font program
|
||||
* bytes, the cmap-selection metrics that drive glyph mapping
|
||||
* (platform_id, encoding_id, type) and the requested subset characters,
|
||||
* so the key combines all of them. The version prefix allows invalidating
|
||||
* stale entries if the subset algorithm changes.
|
||||
*
|
||||
* The font program (potentially several MB) is fingerprinted with xxh128:
|
||||
* this is a content hash purely for cache addressing, not a security
|
||||
* primitive, so a fast non-cryptographic 128-bit hash is sufficient and
|
||||
* keeps cache-hit lookups cheap.
|
||||
*
|
||||
* @param string $font_data Uncompressed font program bytes.
|
||||
* @param TFontData $font Extracted font metrics.
|
||||
* @param array<int, bool> $subchars Subset characters (charcode => enabled).
|
||||
*/
|
||||
protected function subsetCacheKey(string $font_data, array $font, array $subchars): string
|
||||
{
|
||||
\ksort($subchars);
|
||||
|
||||
return (
|
||||
self::SUBSET_CACHE_KEY_PREFIX
|
||||
. \hash('xxh128', $font_data)
|
||||
. ':'
|
||||
. $font['platform_id']
|
||||
. ':'
|
||||
. $font['encoding_id']
|
||||
. ':'
|
||||
. $font['type']
|
||||
. ':'
|
||||
. \hash('xxh128', \implode(',', \array_keys(\array_filter($subchars))))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the PDF output string for fonts
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getFontDefinitions(): string
|
||||
{
|
||||
$out = '';
|
||||
foreach ($this->fonts as $font) {
|
||||
$out .= match (\strtolower($font['type'])) {
|
||||
'core' => $this->getCore($font),
|
||||
'cidfont0' => $this->getCid0($font),
|
||||
'type1' => $this->getTrueType($font),
|
||||
'truetype' => $this->getTrueType($font),
|
||||
'truetypeunicode' => $this->getTrueTypeUnicode($font),
|
||||
default => throw new FontException('Unsupported font type: ' . $font['type']),
|
||||
};
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
+942
@@ -0,0 +1,942 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Stack.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;
|
||||
|
||||
use Com\Tecnick\Pdf\Font\Exception as FontException;
|
||||
use Com\Tecnick\Unicode\Data\BidiClass;
|
||||
use Com\Tecnick\Unicode\Data\Type as UnicodeType;
|
||||
|
||||
/**
|
||||
* Com\Tecnick\Pdf\Font\Stack
|
||||
*
|
||||
* @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 Load
|
||||
*
|
||||
* @phpstan-type TTextSplit array{
|
||||
* 'pos': int,
|
||||
* 'ord': int,
|
||||
* 'spaces': int,
|
||||
* 'septype': string,
|
||||
* 'wordwidth': float,
|
||||
* 'totwidth': float,
|
||||
* 'totspacewidth': float,
|
||||
* }
|
||||
*
|
||||
* @phpstan-type TTextDims array{
|
||||
* 'chars': int,
|
||||
* 'spaces': int,
|
||||
* 'words': int,
|
||||
* 'totwidth': float,
|
||||
* 'totspacewidth': float,
|
||||
* 'split': array<int, TTextSplit>,
|
||||
* }
|
||||
*
|
||||
* @phpstan-type TBBox array{float, float, float, float}
|
||||
*
|
||||
* @phpstan-type TStackItem array{
|
||||
* 'key': string,
|
||||
* 'style': string,
|
||||
* 'size': float,
|
||||
* 'spacing': float,
|
||||
* 'stretching': float,
|
||||
* }
|
||||
*
|
||||
* @phpstan-type TFontMetric array{
|
||||
* 'ascent': float,
|
||||
* 'avgwidth': float,
|
||||
* 'capheight': float,
|
||||
* 'cbbox': array<int, TBBox>,
|
||||
* 'cratio': float,
|
||||
* 'cw': array<int, float>,
|
||||
* 'cwu': array<int, float>,
|
||||
* 'descent': float,
|
||||
* 'dw': float,
|
||||
* 'fbbox': array<int, float>,
|
||||
* 'height': float,
|
||||
* 'idx': int,
|
||||
* 'key': string,
|
||||
* 'maxwidth': float,
|
||||
* 'midpoint': float,
|
||||
* 'missingwidth': float,
|
||||
* 'out': string,
|
||||
* 'outraw': string,
|
||||
* 'size': float,
|
||||
* 'spacing': float,
|
||||
* 'stretching': float,
|
||||
* 'style': string,
|
||||
* 'type': string,
|
||||
* 'up': float,
|
||||
* 'usize': float,
|
||||
* 'ut': float,
|
||||
* 'xheight': float,
|
||||
* }
|
||||
*
|
||||
* @SuppressWarnings("PHPMD.ExcessiveClassComplexity")
|
||||
*/
|
||||
class Stack extends \Com\Tecnick\Pdf\Font\Buffer
|
||||
{
|
||||
/**
|
||||
* Default font size in points
|
||||
*/
|
||||
public const DEFAULT_SIZE = 10;
|
||||
|
||||
/**
|
||||
* Array (stack) containing fonts in order of insertion.
|
||||
* The last item is the current font.
|
||||
*
|
||||
* @var array<int, TStackItem>
|
||||
*/
|
||||
protected array $stack = [];
|
||||
|
||||
/**
|
||||
* Current font index
|
||||
*/
|
||||
protected int $index = -1;
|
||||
|
||||
/**
|
||||
* Array containing font metrics for each fontkey-size combination.
|
||||
*
|
||||
* @var array<string, TFontMetric>
|
||||
*/
|
||||
protected array $metric = [];
|
||||
|
||||
/**
|
||||
* Insert a font into the stack
|
||||
*
|
||||
* The definition file (and the font file itself when embedding) must be present either in the current directory
|
||||
* or in the one indicated by K_PATH_FONTS if the constant is defined.
|
||||
*
|
||||
* @param int $objnum Current PDF object number
|
||||
* @param string $font Font family, or comma separated list of font families
|
||||
* If it is a standard family name, it will override the corresponding font.
|
||||
* @param string $style Font style.
|
||||
* Possible values are (case-insensitive):
|
||||
* regular (default)
|
||||
* B: bold
|
||||
* I: italic
|
||||
* U: underline
|
||||
* D: strikeout (linethrough)
|
||||
* O: overline
|
||||
* @param ?float $size Font size in points (set to null to inherit the last font size).
|
||||
* @param ?float $spacing Extra spacing between characters.
|
||||
* @param ?float $stretching Horizontal character stretching ratio.
|
||||
* @param string $ifile The font definition file (or empty for autodetect).
|
||||
* By default, the name is built from the family and style, in lower case with no spaces.
|
||||
* @param ?bool $subset If true embed only a subset of the font (stores only the information related to
|
||||
* the used characters); If false embed full font; This option is valid only for
|
||||
* TrueTypeUnicode fonts and is disabled for PDF/A. If you want to enable users to
|
||||
* modify the document, set this parameter to false. If you subset the font, the person
|
||||
* who receives your PDF would need to have your same font in order to make changes to
|
||||
* your PDF. The file size of the PDF would also be smaller because you are embedding
|
||||
* only a subset.
|
||||
* Set this to null to use the default value.
|
||||
* NOTE: This option is computational and memory intensive.
|
||||
*
|
||||
* @return TFontMetric Font data
|
||||
*
|
||||
* @throws FontException in case of error
|
||||
*/
|
||||
public function insert(
|
||||
int &$objnum,
|
||||
string $font,
|
||||
string $style = '',
|
||||
?float $size = null,
|
||||
?float $spacing = null,
|
||||
?float $stretching = null,
|
||||
string $ifile = '',
|
||||
?bool $subset = null,
|
||||
): array {
|
||||
if ($subset === null) {
|
||||
$subset = $this->subset;
|
||||
}
|
||||
|
||||
$size = $this->getInputSize($size);
|
||||
$spacing = $this->getInputSpacing($spacing);
|
||||
$stretching = $this->getInputStretching($stretching);
|
||||
|
||||
// try to load the corresponding imported font
|
||||
/** @var ?FontException $err */
|
||||
$err = null;
|
||||
$keys = $this->getNormalizedFontKeys($font);
|
||||
$fontkey = '';
|
||||
foreach ($keys as $key) {
|
||||
try {
|
||||
$fontkey = $this->add($objnum, $key, $style, $ifile, $subset);
|
||||
$err = null;
|
||||
break;
|
||||
} catch (FontException $exc) {
|
||||
$err = $exc;
|
||||
}
|
||||
}
|
||||
|
||||
if ($err !== null) {
|
||||
throw new FontException($err->getMessage());
|
||||
}
|
||||
|
||||
// add this font in the stack
|
||||
$data = $this->getFont($fontkey);
|
||||
|
||||
$this->stack[++$this->index] = [
|
||||
'key' => $fontkey,
|
||||
'style' => $data['style'],
|
||||
'size' => $size,
|
||||
'spacing' => $spacing,
|
||||
'stretching' => $stretching,
|
||||
];
|
||||
|
||||
return $this->getFontMetric($this->index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current font data array.
|
||||
*
|
||||
* @return TFontMetric
|
||||
*
|
||||
* @throws FontException
|
||||
*/
|
||||
public function getCurrentFont(): array
|
||||
{
|
||||
return $this->getFontMetric($this->index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a clone of the specified font with new parameters.
|
||||
*
|
||||
* @param int $objnum Current PDF object number.
|
||||
* @param ?int $idx Font index. Leave it null to use the current font.
|
||||
* @param ?string $style Font style.
|
||||
* Possible values are (case-insensitive):
|
||||
* regular (default)
|
||||
* B: bold
|
||||
* I: italic
|
||||
* U: underline
|
||||
* D: strikeout (linethrough)
|
||||
* O: overline
|
||||
* @param ?float $size Font size in points (set to null to inherit the last font size).
|
||||
* @param ?float $spacing Extra spacing between characters.
|
||||
* @param ?float $stretching Horizontal character stretching ratio.
|
||||
*
|
||||
* @return TFontMetric
|
||||
*
|
||||
* @throws FontException
|
||||
*/
|
||||
public function cloneFont(
|
||||
int &$objnum,
|
||||
?int $idx = null,
|
||||
?string $style = null,
|
||||
?float $size = null,
|
||||
?float $spacing = null,
|
||||
?float $stretching = null,
|
||||
): array {
|
||||
if ($idx === null) {
|
||||
$idx = $this->index;
|
||||
} elseif ($idx < 0 || $idx > $this->index) {
|
||||
throw new FontException('Invalid font index');
|
||||
}
|
||||
|
||||
$curfont = $this->getStackItem($idx);
|
||||
|
||||
if ($style === null || $style === $curfont['style']) {
|
||||
$size = $this->getInputSize($size);
|
||||
$spacing = $this->getInputSpacing($spacing);
|
||||
$stretching = $this->getInputStretching($stretching);
|
||||
|
||||
$this->stack[++$this->index] = [
|
||||
'key' => $curfont['key'],
|
||||
'style' => $curfont['style'],
|
||||
'size' => $size,
|
||||
'spacing' => $spacing,
|
||||
'stretching' => $stretching,
|
||||
];
|
||||
|
||||
return $this->getFontMetric($this->index);
|
||||
}
|
||||
|
||||
$data = $this->getFont($curfont['key']);
|
||||
|
||||
return $this->insert(
|
||||
$objnum,
|
||||
$data['family'],
|
||||
$style,
|
||||
$size,
|
||||
$spacing,
|
||||
$stretching,
|
||||
$this->getStyleFontFile($data, $style),
|
||||
$data['subset'],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the font definition file to use to load a different style of an already loaded font.
|
||||
*
|
||||
* The 'ifile' entry of a loaded font is the definition file of its own style,
|
||||
* so it cannot be reused as is for a different style.
|
||||
* The definition file of the requested style is searched in the same directory of the source one;
|
||||
* if it is not there, an empty string is returned to trigger the standard autodetection,
|
||||
* that also provides the artificial style fallback when no styled definition file exists.
|
||||
*
|
||||
* @param TFontData $data Data of the source font.
|
||||
* @param string $style Requested font style.
|
||||
*
|
||||
* @return string The font definition file, or an empty string for autodetection.
|
||||
*/
|
||||
protected function getStyleFontFile(array $data, string $style): string
|
||||
{
|
||||
if ($data['dir'] === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$style = \strtoupper($style);
|
||||
$suffix = (\str_contains($style, 'B') ? 'B' : '') . (\str_contains($style, 'I') ? 'I' : '');
|
||||
$ifile = $data['dir'] . DIRECTORY_SEPARATOR . \strtolower($data['family'] . $suffix) . '.json';
|
||||
|
||||
return \is_readable($ifile) ? $ifile : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current font key.
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @throws FontException
|
||||
*/
|
||||
public function getCurrentFontKey(): string
|
||||
{
|
||||
return $this->getCurrentStackItem()['key'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current font type (i.e.: Core, TrueType, TrueTypeUnicode, Type1).
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @throws FontException
|
||||
*/
|
||||
public function getCurrentFontType(): string
|
||||
{
|
||||
return $this->getFont($this->getCurrentStackItem()['key'])['type'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a current font is available on the stack.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasCurrentFont(): bool
|
||||
{
|
||||
return $this->index >= 0 && $this->stack !== [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of fonts currently stored in the stack.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getStackSize(): int
|
||||
{
|
||||
return \count($this->stack);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current font index in the stack.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getCurrentFontIndex(): int
|
||||
{
|
||||
return $this->index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the PDF code to use the current font.
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @throws FontException
|
||||
*/
|
||||
public function getOutCurrentFont(): string
|
||||
{
|
||||
return $this->getFontMetric($this->index)['out'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the current font type is Core, TrueType or Type1.
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @throws FontException
|
||||
*/
|
||||
public function isCurrentByteFont(): bool
|
||||
{
|
||||
$currentFontType = $this->getCurrentFontType();
|
||||
return $currentFontType === 'Core' || $currentFontType === 'TrueType' || $currentFontType === 'Type1';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the current font type is TrueTypeUnicode or cidfont0.
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @throws FontException
|
||||
*/
|
||||
public function isCurrentUnicodeFont(): bool
|
||||
{
|
||||
$currentFontType = $this->getCurrentFontType();
|
||||
return $currentFontType === 'TrueTypeUnicode' || $currentFontType === 'cidfont0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove and return the last inserted font
|
||||
*
|
||||
* @return TFontMetric
|
||||
*
|
||||
* @throws FontException
|
||||
*/
|
||||
public function popLastFont(): array
|
||||
{
|
||||
if ($this->index < 0 || $this->stack === []) {
|
||||
throw new FontException('The font stack is empty');
|
||||
}
|
||||
|
||||
$font = $this->getFontMetric($this->index);
|
||||
\array_pop($this->stack);
|
||||
--$this->index;
|
||||
return $font;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace missing characters with selected substitutions
|
||||
*
|
||||
* @param array<int, int> $uniarr Array of character codepoints.
|
||||
* @param array<int, array<int>> $subs Array of possible character substitutions.
|
||||
* The key is the character to check (integer value),
|
||||
* the value is an array of possible substitutes.
|
||||
*
|
||||
* @return array<int, int> Array of character codepoints.
|
||||
*
|
||||
* @throws FontException
|
||||
*/
|
||||
public function replaceMissingChars(array $uniarr, array $subs = []): array
|
||||
{
|
||||
$font = $this->getFontMetric($this->index);
|
||||
foreach ($uniarr as $pos => $uni) {
|
||||
if (isset($font['cw'][$uni])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$alts = $subs[$uni] ?? null;
|
||||
if ($alts === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($alts as $alt) {
|
||||
if (!isset($font['cw'][$alt])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$uniarr[$pos] = $alt;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $uniarr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the specified Unicode value is defined in the current font
|
||||
*
|
||||
* @param int $ord Unicode character value to convert
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @throws FontException
|
||||
*/
|
||||
public function isCharDefined(int $ord): bool
|
||||
{
|
||||
$font = $this->getFontMetric($this->index);
|
||||
return isset($font['cw'][$ord]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the width of the specified character
|
||||
*
|
||||
* @param int $ord Unicode character value.
|
||||
*
|
||||
* @return float
|
||||
*
|
||||
* @throws FontException
|
||||
*/
|
||||
public function getCharWidth(int $ord): float
|
||||
{
|
||||
if ($ord === 173 || $ord === 8203) {
|
||||
// 173 = SHY character is not printed, as it is used for text hyphenation
|
||||
// 8203 = ZWSP character
|
||||
return 0;
|
||||
}
|
||||
|
||||
$font = $this->getFontMetric($this->index);
|
||||
if (isset($font['cwu'][$ord])) {
|
||||
return $font['cwu'][$ord];
|
||||
}
|
||||
|
||||
return $font['cw'][$ord] ?? $font['dw'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the length of the string specified using an array of codepoints.
|
||||
*
|
||||
* @param array<int, int> $uniarr Array of character codepoints.
|
||||
*
|
||||
* @return float
|
||||
*
|
||||
* @throws FontException
|
||||
*/
|
||||
public function getOrdArrWidth(array $uniarr): float
|
||||
{
|
||||
return $this->getOrdArrDims($uniarr)['totwidth'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns various dimensions of the string specified using an array of codepoints.
|
||||
*
|
||||
* @param array<int, int> $uniarr Array of character codepoints.
|
||||
*
|
||||
* @return TTextDims
|
||||
*
|
||||
* @throws FontException
|
||||
*/
|
||||
public function getOrdArrDims(array $uniarr): array
|
||||
{
|
||||
$chars = \count($uniarr); // total number of chars
|
||||
$spaces = 0; // total number of spaces
|
||||
$totwidth = 0; // total string width
|
||||
$totspacewidth = 0; // total space width
|
||||
$words = 0; // total number of words
|
||||
$curfont = $this->getFontMetric($this->index);
|
||||
$fact = $curfont['spacing'] * $curfont['stretching'];
|
||||
$fkey = $curfont['key'];
|
||||
$subset = false;
|
||||
if (
|
||||
isset($this->font[$fkey])
|
||||
&& \is_array($this->font[$fkey])
|
||||
&& \array_key_exists('subset', $this->font[$fkey])
|
||||
&& $this->font[$fkey]['subset'] === true
|
||||
) {
|
||||
$subset = true;
|
||||
}
|
||||
$uniarr[] = 8203; // add null at the end to ensure that the last word is processed
|
||||
$split = [];
|
||||
$prevtotwidth = 0.0;
|
||||
foreach ($uniarr as $idx => $ord) {
|
||||
if ($subset) {
|
||||
$this->addSubsetChar($fkey, $ord);
|
||||
}
|
||||
|
||||
// getType() resolves the code points that are not listed in the type table,
|
||||
// which only holds the ones whose bidirectional type is not L.
|
||||
$unitype = UnicodeType::getType($ord);
|
||||
$bidiClass = BidiClass::tryFrom($unitype);
|
||||
// Inline the width lookup using the already-resolved $curfont metric: calling
|
||||
// getCharWidth() here would re-resolve getFontMetric($this->index) per character.
|
||||
$chrwidth = match ($ord) {
|
||||
173, 8203 => 0.0, // 173 = SHY (hyphenation), 8203 = ZWSP: not printed
|
||||
default => $curfont['cwu'][$ord] ?? $curfont['cw'][$ord] ?? $curfont['dw'],
|
||||
};
|
||||
// Split on paragraph/segment separators (B, S), whitespace (WS) and boundary neutrals (BN).
|
||||
if (
|
||||
$bidiClass === BidiClass::B
|
||||
|| $bidiClass === BidiClass::S
|
||||
|| $bidiClass === BidiClass::WS
|
||||
|| $bidiClass === BidiClass::BN
|
||||
) {
|
||||
$currenttotwidth = $totwidth + ($fact * ($idx - 1));
|
||||
$split[$words] = [
|
||||
'pos' => $idx,
|
||||
'ord' => $ord,
|
||||
'spaces' => $spaces,
|
||||
'septype' => $unitype,
|
||||
'wordwidth' => $words > 0 ? $currenttotwidth - $prevtotwidth : 0,
|
||||
'totwidth' => $currenttotwidth,
|
||||
'totspacewidth' => $totspacewidth + ($fact * \max(0, $spaces - 1)),
|
||||
];
|
||||
$prevtotwidth = $currenttotwidth;
|
||||
$words++;
|
||||
if ($bidiClass === BidiClass::WS) {
|
||||
++$spaces;
|
||||
$totspacewidth += $chrwidth;
|
||||
}
|
||||
}
|
||||
$totwidth += $chrwidth;
|
||||
}
|
||||
$totwidth += $fact * \max(0, $chars - 1);
|
||||
$totspacewidth += $fact * \max(0, $spaces - 1);
|
||||
return [
|
||||
'chars' => $chars,
|
||||
'spaces' => $spaces,
|
||||
'words' => $words,
|
||||
'totwidth' => $totwidth,
|
||||
'totspacewidth' => $totspacewidth,
|
||||
'split' => $split,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the glyph bounding box of the specified character in the current font in user units.
|
||||
*
|
||||
* @param int $ord Unicode character value.
|
||||
*
|
||||
* @return TBBox (xMin, yMin, xMax, yMax)
|
||||
*
|
||||
* @throws FontException
|
||||
*/
|
||||
public function getCharBBox(int $ord): array
|
||||
{
|
||||
$font = $this->getFontMetric($this->index);
|
||||
return $font['cbbox'][$ord] ?? [0.0, 0.0, 0.0, 0.0]; // glyph without outline
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace a char if it is defined on the current font.
|
||||
*
|
||||
* @param int $oldchar Integer code (Unicode) of the character to replace.
|
||||
* @param int $newchar Integer code (Unicode) of the new character.
|
||||
*
|
||||
* @return int the replaced char or the old char in case the new char is not defined
|
||||
*
|
||||
* @throws FontException
|
||||
*/
|
||||
public function replaceChar(int $oldchar, int $newchar): int
|
||||
{
|
||||
if ($this->isCharDefined($newchar)) {
|
||||
// add the new char on the subset list
|
||||
$this->addSubsetChar($this->getFontMetric($this->index)['key'], $newchar);
|
||||
// return the new character
|
||||
return $newchar;
|
||||
}
|
||||
|
||||
// return the old char
|
||||
return $oldchar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the font metrics associated to the input key.
|
||||
*
|
||||
* @param int $idx Font index in the stack.
|
||||
*
|
||||
* @return TFontMetric
|
||||
*
|
||||
* @throws FontException
|
||||
*/
|
||||
protected function getFontMetric(int $idx): array
|
||||
{
|
||||
$font = $this->getStackItem($idx);
|
||||
// Cheap, collision-free cache key from just the fields the metric depends on,
|
||||
// instead of md5(serialize($font)) which ran on every (mostly cache-hit) call.
|
||||
$mkey =
|
||||
$font['key']
|
||||
. '|'
|
||||
. $font['size']
|
||||
. '|'
|
||||
. $font['spacing']
|
||||
. '|'
|
||||
. $font['stretching']
|
||||
. '|'
|
||||
. $font['style'];
|
||||
if (isset($this->metric[$mkey])) {
|
||||
return $this->metric[$mkey];
|
||||
}
|
||||
|
||||
$fontkey = $font['key'];
|
||||
$fontsize = $font['size'];
|
||||
$fontspacing = $font['spacing'];
|
||||
$fontstretching = $font['stretching'];
|
||||
$fontstyle = $font['style'];
|
||||
|
||||
$usize = $fontsize / $this->kunit;
|
||||
$cratio = $fontsize / 1000;
|
||||
$wratio = $cratio * $fontstretching; // horizontal ratio
|
||||
$data = $this->getFont($fontkey);
|
||||
$desc = $data['desc'];
|
||||
// Build the glyph widths and bounding boxes already scaled to internal units in a
|
||||
// single pass, instead of first casting into intermediate arrays and then rescaling.
|
||||
$cw = [];
|
||||
foreach ($data['cw'] as $cid => $width) {
|
||||
$cw[(int) $cid] = (float) $width * $wratio;
|
||||
}
|
||||
|
||||
$cwu = [];
|
||||
foreach ($data['cwu'] as $codepoint => $width) {
|
||||
$cwu[(int) $codepoint] = (float) $width * $wratio;
|
||||
}
|
||||
|
||||
$cbbox = [];
|
||||
foreach ($data['cbbox'] as $cid => $val) {
|
||||
if (\count($val) !== 4) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$bbox = \array_values($val);
|
||||
$cbbox[(int) $cid] = [
|
||||
0 => (float) $bbox[0] * $wratio,
|
||||
1 => (float) $bbox[1] * $cratio,
|
||||
2 => (float) $bbox[2] * $wratio,
|
||||
3 => (float) $bbox[3] * $cratio,
|
||||
];
|
||||
}
|
||||
|
||||
$ascent = (float) $desc['Ascent'];
|
||||
$descent = (float) $desc['Descent'];
|
||||
$avgwidth = (float) $desc['AvgWidth'];
|
||||
$capheight = (float) $desc['CapHeight'];
|
||||
$maxwidth = (float) $desc['MaxWidth'];
|
||||
$missingwidth = (float) $desc['MissingWidth'];
|
||||
$xheight = (float) $desc['XHeight'];
|
||||
$fontbbox = $desc['FontBBox'];
|
||||
$dw = (float) $data['dw'];
|
||||
$up = (float) $data['up'];
|
||||
$ut = (float) $data['ut'];
|
||||
$fonttype = $data['type'];
|
||||
$outfont = \sprintf('/F%d %F Tf', (int) $data['i'], $fontsize); // PDF output string
|
||||
$tbox = \array_pad(\explode(' ', \substr($fontbbox, 1, -1)), 4, '0');
|
||||
// add this font in the stack with metrics in internal units
|
||||
$this->metric[$mkey] = [
|
||||
'ascent' => $ascent * $cratio,
|
||||
'avgwidth' => $avgwidth * $cratio * $fontstretching,
|
||||
'capheight' => $capheight * $cratio,
|
||||
'cbbox' => $cbbox,
|
||||
'cratio' => $cratio,
|
||||
'cw' => $cw,
|
||||
'cwu' => $cwu,
|
||||
'descent' => $descent * $cratio,
|
||||
'dw' => $dw * $cratio * $fontstretching,
|
||||
'fbbox' => [
|
||||
0 => (\is_numeric($tbox[0]) ? (float) $tbox[0] : 0.0) * $wratio, // left
|
||||
1 => (\is_numeric($tbox[1]) ? (float) $tbox[1] : 0.0) * $cratio, // bottom
|
||||
2 => (\is_numeric($tbox[2]) ? (float) $tbox[2] : 0.0) * $wratio, // right
|
||||
3 => (\is_numeric($tbox[3]) ? (float) $tbox[3] : 0.0) * $cratio, // top
|
||||
],
|
||||
'height' => ($ascent - $descent) * $cratio,
|
||||
'idx' => $idx,
|
||||
'key' => $fontkey,
|
||||
'maxwidth' => $maxwidth * $cratio * $fontstretching,
|
||||
'midpoint' => (($ascent + $descent) * $cratio) / 2,
|
||||
'missingwidth' => $missingwidth * $cratio * $fontstretching,
|
||||
'out' => 'BT ' . $outfont . ' ET' . "\r",
|
||||
'outraw' => $outfont,
|
||||
'size' => $fontsize,
|
||||
'spacing' => $fontspacing,
|
||||
'stretching' => $fontstretching,
|
||||
'style' => $fontstyle,
|
||||
'type' => $fonttype,
|
||||
'up' => $up * $cratio,
|
||||
'usize' => $usize,
|
||||
'ut' => $ut * $cratio,
|
||||
'xheight' => $xheight * $cratio,
|
||||
];
|
||||
return $this->metric[$mkey];
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize the input size (minimum 0)
|
||||
*
|
||||
* @param ?float $size Font size in points (set to null to inherit the last font size).
|
||||
*
|
||||
* @return float
|
||||
*
|
||||
* @throws FontException
|
||||
*/
|
||||
protected function getInputSize(?float $size = null): float
|
||||
{
|
||||
if ($size === null || $size < 0) {
|
||||
if ($this->index >= 0) {
|
||||
// inherit the size of the last inserted font
|
||||
return $this->getCurrentStackItem()['size'];
|
||||
}
|
||||
|
||||
return self::DEFAULT_SIZE;
|
||||
}
|
||||
|
||||
return \max(0, $size);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize the input spacing (minimum 0)
|
||||
*
|
||||
* @param ?float $spacing Extra spacing between characters.
|
||||
*
|
||||
* @return float
|
||||
*
|
||||
* @throws FontException
|
||||
*/
|
||||
protected function getInputSpacing(?float $spacing = null): float
|
||||
{
|
||||
if ($spacing === null) {
|
||||
if ($this->index >= 0) {
|
||||
// inherit the size of the last inserted font
|
||||
return $this->getCurrentStackItem()['spacing'];
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
return $spacing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize the input stretching
|
||||
*
|
||||
* @param ?float $stretching Horizontal character stretching ratio.
|
||||
*
|
||||
* @return float
|
||||
*
|
||||
* @throws FontException
|
||||
*/
|
||||
protected function getInputStretching(?float $stretching = null): float
|
||||
{
|
||||
if ($stretching === null) {
|
||||
if ($this->index >= 0) {
|
||||
// inherit the size of the last inserted font
|
||||
return $this->getCurrentStackItem()['stretching'];
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
return $stretching;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the stack item at the given index.
|
||||
*
|
||||
* @param int $idx Font index in the stack.
|
||||
*
|
||||
* @return TStackItem
|
||||
*
|
||||
* @throws FontException
|
||||
*/
|
||||
protected function getStackItem(int $idx): array
|
||||
{
|
||||
$item = $this->stack[$idx] ?? null;
|
||||
if ($item === null) {
|
||||
throw new FontException('Invalid font index');
|
||||
}
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current stack item.
|
||||
*
|
||||
* @return TStackItem
|
||||
*
|
||||
* @throws FontException
|
||||
*/
|
||||
protected function getCurrentStackItem(): array
|
||||
{
|
||||
return $this->getStackItem($this->index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return normalized font keys
|
||||
*
|
||||
* @param string $fontfamily Property string containing comma-separated font family names
|
||||
*
|
||||
* @return array<string>
|
||||
*
|
||||
* @throws FontException
|
||||
*/
|
||||
protected function getNormalizedFontKeys(string $fontfamily): array
|
||||
{
|
||||
if ($fontfamily === '') {
|
||||
throw new FontException('Empty font family name');
|
||||
}
|
||||
|
||||
$keys = [];
|
||||
// remove spaces and symbols
|
||||
$fontfamily = \preg_replace('/[^a-z0-9_\,]/', '', \strtolower($fontfamily));
|
||||
if ($fontfamily === null) {
|
||||
throw new FontException('Invalid font family name');
|
||||
}
|
||||
|
||||
// extract all font names
|
||||
$fontslist = \preg_split('/[,]/', $fontfamily);
|
||||
if ($fontslist === false) {
|
||||
throw new FontException('Invalid font family name: ' . $fontfamily);
|
||||
}
|
||||
|
||||
// replacement patterns
|
||||
|
||||
$fontpattern = ['/regular$/', '/italic$/', '/oblique$/', '/bold([I]?)$/'];
|
||||
$fontreplacement = ['', 'I', 'I', 'B\\1'];
|
||||
|
||||
$keypattern = ['/^serif|^cursive|^fantasy|^timesnewroman/', '/^sansserif/', '/^monospace/'];
|
||||
$keyreplacement = ['times', 'helvetica', 'courier'];
|
||||
|
||||
// find first valid font name
|
||||
foreach ($fontslist as $font) {
|
||||
$font = \preg_replace($fontpattern, $fontreplacement, $font);
|
||||
if ($font === null) {
|
||||
throw new FontException('Invalid font family name: ' . $fontfamily);
|
||||
}
|
||||
|
||||
// replace common family names and core fonts
|
||||
$fontkey = \preg_replace($keypattern, $keyreplacement, $font);
|
||||
if ($fontkey === null) {
|
||||
throw new FontException('Invalid font family name: ' . $fontfamily);
|
||||
}
|
||||
|
||||
$keys[] = $fontkey;
|
||||
}
|
||||
|
||||
return $keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the normalized font family name or the current font name key.
|
||||
*
|
||||
* @param string $fontfamily Raw font family name.
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @throws FontException
|
||||
*/
|
||||
public function getFontFamilyName(string $fontfamily): string
|
||||
{
|
||||
$fkeys = $this->getNormalizedFontKeys($fontfamily);
|
||||
foreach ($fkeys as $fkey) {
|
||||
if ($this->isValidKey($fkey)) {
|
||||
return $fkey;
|
||||
}
|
||||
|
||||
$pdfakey = 'pdfa' . $fkey;
|
||||
if ($this->isValidKey($pdfakey)) {
|
||||
return $pdfakey;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->getCurrentFontKey();
|
||||
}
|
||||
}
|
||||
+580
@@ -0,0 +1,580 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Subset.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;
|
||||
|
||||
use Com\Tecnick\File\Byte;
|
||||
use Com\Tecnick\File\File as ObjFile;
|
||||
use Com\Tecnick\Pdf\Font\Exception as FontException;
|
||||
use Com\Tecnick\Pdf\Font\Import\TrueType;
|
||||
|
||||
/**
|
||||
* Com\Tecnick\Pdf\Font\Subset
|
||||
*
|
||||
* @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 Load
|
||||
*/
|
||||
class Subset
|
||||
{
|
||||
/**
|
||||
* array of table names to preserve (loca and glyf tables will be added later)
|
||||
* the cmap table is not needed and shall not be present,
|
||||
* since the mapping from character codes to glyph descriptions is provided separately
|
||||
*
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
protected const TABLENAMES = [
|
||||
'head' => true,
|
||||
'hhea' => true,
|
||||
'hmtx' => true,
|
||||
'maxp' => true,
|
||||
'cvt ' => true,
|
||||
'fpgm' => true,
|
||||
'prep' => true,
|
||||
'glyf' => true,
|
||||
'loca' => true,
|
||||
];
|
||||
|
||||
/**
|
||||
* Content of the input font file
|
||||
*/
|
||||
protected string $font = '';
|
||||
|
||||
/**
|
||||
* Object used to read font bytes
|
||||
*/
|
||||
protected Byte $fbyte;
|
||||
|
||||
/**
|
||||
* Extracted font metrics
|
||||
*
|
||||
* @var TFontData
|
||||
*/
|
||||
protected array $fdt = [
|
||||
'Ascender' => 0,
|
||||
'Ascent' => 0,
|
||||
'AvgWidth' => 0.0,
|
||||
'CapHeight' => 0,
|
||||
'CharacterSet' => '',
|
||||
'Descender' => 0,
|
||||
'Descent' => 0,
|
||||
'EncodingScheme' => '',
|
||||
'FamilyName' => '',
|
||||
'Flags' => 0,
|
||||
'FontBBox' => [],
|
||||
'FontName' => '',
|
||||
'FullName' => '',
|
||||
'IsFixedPitch' => false,
|
||||
'ItalicAngle' => 0,
|
||||
'Leading' => 0,
|
||||
'MaxWidth' => 0,
|
||||
'MissingWidth' => 0,
|
||||
'StdHW' => 0,
|
||||
'StdVW' => 0,
|
||||
'StemH' => 0,
|
||||
'StemV' => 0,
|
||||
'UnderlinePosition' => 0,
|
||||
'UnderlineThickness' => 0,
|
||||
'Version' => '',
|
||||
'Weight' => '',
|
||||
'XHeight' => 0,
|
||||
'bbox' => '',
|
||||
'cbbox' => [],
|
||||
'cidinfo' => [
|
||||
'Ordering' => '',
|
||||
'Registry' => '',
|
||||
'Supplement' => 0,
|
||||
'uni2cid' => [],
|
||||
],
|
||||
'compress' => false,
|
||||
'ctg' => '',
|
||||
'ctgdata' => [],
|
||||
'cw' => [],
|
||||
'cwu' => [],
|
||||
'datafile' => '',
|
||||
'desc' => [
|
||||
'Ascent' => 0,
|
||||
'AvgWidth' => 0,
|
||||
'CapHeight' => 0,
|
||||
'Descent' => 0,
|
||||
'Flags' => 0,
|
||||
'FontBBox' => '',
|
||||
'ItalicAngle' => 0,
|
||||
'Leading' => 0,
|
||||
'MaxWidth' => 0,
|
||||
'MissingWidth' => 0,
|
||||
'StemH' => 0,
|
||||
'StemV' => 0,
|
||||
'XHeight' => 0,
|
||||
],
|
||||
'diff' => '',
|
||||
'diff_n' => 0,
|
||||
'dir' => '',
|
||||
'dw' => 0,
|
||||
'enc' => '',
|
||||
'enc_map' => [],
|
||||
'encodingTables' => [],
|
||||
'encoding_id' => 0,
|
||||
'encrypted' => '',
|
||||
'fakestyle' => false,
|
||||
'family' => '',
|
||||
'file' => '',
|
||||
'file_n' => 0,
|
||||
'file_name' => '',
|
||||
'i' => 0,
|
||||
'ifile' => '',
|
||||
'indexToLoc' => [],
|
||||
'input_file' => '',
|
||||
'isUnicode' => false,
|
||||
'italicAngle' => 0,
|
||||
'key' => '',
|
||||
'lenIV' => 0,
|
||||
'length1' => 0,
|
||||
'length2' => 0,
|
||||
'linked' => false,
|
||||
'mode' => [
|
||||
'bold' => false,
|
||||
'italic' => false,
|
||||
'linethrough' => false,
|
||||
'overline' => false,
|
||||
'underline' => false,
|
||||
],
|
||||
'n' => 0,
|
||||
'name' => '',
|
||||
'numGlyphs' => 0,
|
||||
'numHMetrics' => 0,
|
||||
'originalsize' => 0,
|
||||
'pdfa' => false,
|
||||
'platform_id' => 0,
|
||||
'settype' => '',
|
||||
'short_offset' => false,
|
||||
'size1' => 0,
|
||||
'size2' => 0,
|
||||
'style' => '',
|
||||
'subset' => false,
|
||||
'subsetchars' => [],
|
||||
'table' => [],
|
||||
'tot_num_glyphs' => 0,
|
||||
'type' => '',
|
||||
'underlinePosition' => 0,
|
||||
'underlineThickness' => 0,
|
||||
'unicode' => false,
|
||||
'unitsPerEm' => 0,
|
||||
'up' => 0,
|
||||
'urk' => 0.0,
|
||||
'ut' => 0,
|
||||
'weight' => '',
|
||||
];
|
||||
|
||||
/**
|
||||
* Array containing subset glyphs indexes of chars from cmap table
|
||||
*
|
||||
* @var array<int, bool>
|
||||
*/
|
||||
protected array $subglyphs = [];
|
||||
|
||||
/**
|
||||
* Subset font
|
||||
*/
|
||||
protected string $subfont = '';
|
||||
|
||||
/**
|
||||
* Pointer position on the original font data
|
||||
*/
|
||||
protected int $offset = 0;
|
||||
|
||||
/**
|
||||
* File helper used to load font definition files.
|
||||
*/
|
||||
protected ObjFile $fileHelper;
|
||||
|
||||
/**
|
||||
* Process TrueType font
|
||||
*
|
||||
* @param string $font Content of the input font file
|
||||
* @param TFontData $fdt Extracted font metrics
|
||||
* @param ObjFile $fileHelper Optional file helper for font loading.
|
||||
* @param array<int, bool> $subchars Array containing subset chars
|
||||
*
|
||||
* @throws FontException in case of error
|
||||
*/
|
||||
public function __construct(string $font, array $fdt, ObjFile $fileHelper, array $subchars = [])
|
||||
{
|
||||
$this->fileHelper = $fileHelper;
|
||||
$this->font = $font;
|
||||
$this->fbyte = new Byte($font);
|
||||
$trueType = new TrueType(
|
||||
font: $font,
|
||||
fdt: $fdt,
|
||||
fileHelper: $this->fileHelper,
|
||||
fbyte: $this->fbyte,
|
||||
subchars: $subchars,
|
||||
// Subsetting only needs the glyph program (loca/glyf); per-glyph bounding
|
||||
// boxes are never read here, so skip computing them.
|
||||
withCbbox: false,
|
||||
);
|
||||
$this->fdt = $trueType->getFontMetrics();
|
||||
$this->subglyphs = $trueType->getSubGlyphs();
|
||||
$this->addCompositeGlyphs();
|
||||
$this->addProcessedTables();
|
||||
$this->removeUnusedTables();
|
||||
$this->buildSubsetFont();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the extracted font metrics
|
||||
*/
|
||||
public function getSubsetFont(): string
|
||||
{
|
||||
return $this->subfont;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the checksum of a TTF table.
|
||||
*
|
||||
* @param string $table Table to check
|
||||
* @param int $length Length of table in bytes
|
||||
*
|
||||
* @return int checksum
|
||||
*
|
||||
* @throws FontException
|
||||
*/
|
||||
protected function getTableChecksum(string $table, int $length): int
|
||||
{
|
||||
$sum = 0;
|
||||
$tlen = (int) floor(($length + 3) / 4);
|
||||
$offset = 0;
|
||||
for ($idx = 0; $idx < $tlen; ++$idx) {
|
||||
$chunk = \substr($table, $offset, 4);
|
||||
if (\strlen($chunk) < 4) {
|
||||
// OpenType checksums use zero-padding for trailing partial words.
|
||||
$chunk = \str_pad($chunk, 4, "\0", STR_PAD_RIGHT);
|
||||
}
|
||||
|
||||
$val = \unpack('Ni', $chunk);
|
||||
if ($val === false) {
|
||||
throw new FontException('Unable to unpack table data');
|
||||
}
|
||||
|
||||
$sum += $val['i'];
|
||||
$offset += 4;
|
||||
}
|
||||
|
||||
$sum = \unpack('Ni', \pack('N', $sum));
|
||||
if ($sum === false) {
|
||||
throw new FontException('Unable to unpack checksum');
|
||||
}
|
||||
|
||||
return $sum['i'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add composite glyphs
|
||||
*/
|
||||
protected function addCompositeGlyphs(): void
|
||||
{
|
||||
$new_sga = $this->subglyphs;
|
||||
while ($new_sga !== []) {
|
||||
$sga = \array_keys($new_sga);
|
||||
$new_sga = [];
|
||||
foreach ($sga as $key) {
|
||||
$new_sga = $this->findCompositeGlyphs($new_sga, $key);
|
||||
}
|
||||
|
||||
foreach ($new_sga as $gid => $enabled) {
|
||||
if (!$enabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->subglyphs[$gid] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// sort glyphs by key
|
||||
\ksort($this->subglyphs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find composite glyphs
|
||||
*
|
||||
* @param array<int, bool> $new_sga
|
||||
* @param int $key
|
||||
*
|
||||
* @return array<int, bool>
|
||||
*/
|
||||
protected function findCompositeGlyphs(array $new_sga, int $key): array
|
||||
{
|
||||
if (isset($this->fdt['indexToLoc'][$key])) {
|
||||
/**
|
||||
* Glyph Header
|
||||
* - int16 numberOfContours Normal glyph if >= 0 or composite glyph if negative (should be -1)
|
||||
* - int16 xMin Minimum x for coordinate data.
|
||||
* - int16 yMin Minimum y for coordinate data.
|
||||
* - int16 xMax Maximum x for coordinate data.
|
||||
* - int16 yMax Maximum y for coordinate data.
|
||||
*/
|
||||
|
||||
$this->offset = $this->fdt['table']['glyf']['offset'] + $this->fdt['indexToLoc'][$key];
|
||||
$numberOfContours = $this->fbyte->getShort($this->offset);
|
||||
$this->offset += 2;
|
||||
if ($numberOfContours < 0) { // composite glyph
|
||||
/**
|
||||
* ComponentGlyph record
|
||||
* - uint16 flags Normal glyph if >= 0 or composite glyph if negative (should be -1)
|
||||
* - uint16 glyphIndex glyph index of component
|
||||
* - u/int8|u/int16 argument1 x-offset for component or point number; type depends on bits 0 and 1 in component flags
|
||||
* - u/int8|u/int16 argument2 y-offset for component or point number; type depends on bits 0 and 1 in component flags
|
||||
* - [transform data] optional transform data
|
||||
*/
|
||||
$this->offset += 8; // skip xMin, yMin, xMax, yMax
|
||||
do {
|
||||
$flags = $this->fbyte->getUShort($this->offset);
|
||||
$this->offset += 2;
|
||||
$glyphIndex = $this->fbyte->getUShort($this->offset);
|
||||
$this->offset += 2;
|
||||
if (!isset($this->subglyphs[$glyphIndex])) {
|
||||
// add missing glyphs
|
||||
$new_sga[$glyphIndex] = true;
|
||||
}
|
||||
|
||||
// skip some bytes by case
|
||||
// ARG_1_AND_2_ARE_WORDS (bit 0): [u]int32 if set and [u]int16 if not set
|
||||
if (($flags & 1) !== 0) {
|
||||
$this->offset += 4;
|
||||
} else {
|
||||
$this->offset += 2;
|
||||
}
|
||||
|
||||
if (($flags & 8) !== 0) {
|
||||
// WE_HAVE_A_SCALE (bit 3): Adds 1 * F2DOT14 field
|
||||
$this->offset += 2;
|
||||
} elseif (($flags & 64) !== 0) {
|
||||
// WE_HAVE_AN_X_AND_Y_SCALE (bit 6): Adds 2 * F2DOT14 fields
|
||||
$this->offset += 4;
|
||||
} elseif (($flags & 128) !== 0) {
|
||||
// WE_HAVE_A_TWO_BY_TWO (bit 7): Adds 4 * F2DOT14 fields
|
||||
$this->offset += 8;
|
||||
}
|
||||
} while ($flags & 32); // MORE_COMPONENTS (bit 5)
|
||||
}
|
||||
}
|
||||
|
||||
return $new_sga;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove unused tables
|
||||
*/
|
||||
protected function removeUnusedTables(): void
|
||||
{
|
||||
// get the tables to preserve
|
||||
$this->offset = 12;
|
||||
$tabname = \array_keys($this->fdt['table']);
|
||||
foreach ($tabname as $tag) {
|
||||
if (!isset(self::TABLENAMES[$tag])) {
|
||||
// remove the table
|
||||
unset($this->fdt['table'][$tag]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($this->fdt['table'][$tag])) {
|
||||
$this->fdt['table'][$tag] = [
|
||||
'checkSum' => 0,
|
||||
'data' => '',
|
||||
'length' => 0,
|
||||
'offset' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
$isSubsetTable = $tag === 'loca' || $tag === 'glyf';
|
||||
if (!$isSubsetTable) {
|
||||
$this->fdt['table'][$tag]['data'] = \substr(
|
||||
$this->font,
|
||||
$this->fdt['table'][$tag]['offset'],
|
||||
$this->fdt['table'][$tag]['length'],
|
||||
);
|
||||
if ($tag === 'head') {
|
||||
// set the checkSumAdjustment to 0
|
||||
$this->fdt['table'][$tag]['data'] =
|
||||
\substr($this->fdt['table'][$tag]['data'], 0, 8)
|
||||
. "\x0\x0\x0\x0"
|
||||
. \substr($this->fdt['table'][$tag]['data'], 12);
|
||||
}
|
||||
}
|
||||
|
||||
$pad = 4 - ((int) $this->fdt['table'][$tag]['length'] % 4);
|
||||
if ($pad !== 4) {
|
||||
// the length of a table must be a multiple of four bytes
|
||||
$this->fdt['table'][$tag]['length'] += (int) $pad;
|
||||
$this->fdt['table'][$tag]['data'] .= \str_repeat("\x0", max(0, $pad));
|
||||
}
|
||||
|
||||
$this->fdt['table'][$tag]['offset'] = $this->offset;
|
||||
$this->offset += $this->fdt['table'][$tag]['length'];
|
||||
|
||||
// check sum is not changed
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add glyf and loca tables
|
||||
*
|
||||
* @SuppressWarnings("PHPMD.CyclomaticComplexity")
|
||||
* @SuppressWarnings("PHPMD.NPathComplexity")
|
||||
*/
|
||||
protected function addProcessedTables(): void
|
||||
{
|
||||
// build new glyf and loca tables
|
||||
$glyf = '';
|
||||
$loca = '';
|
||||
$this->offset = 0;
|
||||
$glyf_offset = $this->fdt['table']['glyf']['offset'];
|
||||
for ($i = 0; $i < $this->fdt['tot_num_glyphs']; ++$i) {
|
||||
$nextidx = $this->getNextLocaIndex($i + 1);
|
||||
if (isset($this->subglyphs[$i], $this->fdt['indexToLoc'][$i]) && $nextidx !== null) {
|
||||
$length = $this->fdt['indexToLoc'][$nextidx] - $this->fdt['indexToLoc'][$i];
|
||||
$glyf .= \substr($this->font, $glyf_offset + $this->fdt['indexToLoc'][$i], $length);
|
||||
} else {
|
||||
$length = 0;
|
||||
}
|
||||
|
||||
if ($this->fdt['short_offset']) {
|
||||
$loca .= \pack('n', \floor($this->offset / 2));
|
||||
} else {
|
||||
$loca .= \pack('N', $this->offset);
|
||||
}
|
||||
|
||||
$this->offset += $length;
|
||||
}
|
||||
|
||||
// add loca
|
||||
if (!isset($this->fdt['table']['loca'])) {
|
||||
$this->fdt['table']['loca'] = [
|
||||
'checkSum' => 0,
|
||||
'data' => '',
|
||||
'length' => 0,
|
||||
'offset' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
$this->fdt['table']['loca']['data'] = $loca;
|
||||
$this->fdt['table']['loca']['length'] = \strlen($loca);
|
||||
$this->fdt['table']['loca']['offset'] = $this->offset;
|
||||
$pad = 4 - ($this->fdt['table']['loca']['length'] % 4);
|
||||
if ($pad !== 4) {
|
||||
// the length of a table must be a multiple of four bytes
|
||||
$this->fdt['table']['loca']['length'] += $pad;
|
||||
$this->fdt['table']['loca']['data'] .= \str_repeat("\x0", $pad);
|
||||
}
|
||||
|
||||
$this->fdt['table']['loca']['checkSum'] = $this->getTableChecksum(
|
||||
$this->fdt['table']['loca']['data'],
|
||||
$this->fdt['table']['loca']['length'],
|
||||
);
|
||||
|
||||
$this->offset += $this->fdt['table']['loca']['length'];
|
||||
|
||||
// add glyf
|
||||
if (!isset($this->fdt['table']['glyf'])) {
|
||||
$this->fdt['table']['glyf'] = [
|
||||
'checkSum' => 0,
|
||||
'data' => '',
|
||||
'length' => 0,
|
||||
'offset' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
$this->fdt['table']['glyf']['data'] = $glyf;
|
||||
$this->fdt['table']['glyf']['length'] = \strlen($glyf);
|
||||
$this->fdt['table']['glyf']['offset'] = $this->offset;
|
||||
$pad = 4 - ($this->fdt['table']['glyf']['length'] % 4);
|
||||
if ($pad !== 4) {
|
||||
// the length of a table must be a multiple of four bytes
|
||||
$this->fdt['table']['glyf']['length'] += $pad;
|
||||
$this->fdt['table']['glyf']['data'] .= \str_repeat("\x0", $pad);
|
||||
}
|
||||
|
||||
$this->fdt['table']['glyf']['checkSum'] = $this->getTableChecksum(
|
||||
$this->fdt['table']['glyf']['data'],
|
||||
$this->fdt['table']['glyf']['length'],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first available loca index from $start, or null if none exists.
|
||||
*/
|
||||
protected function getNextLocaIndex(int $start): ?int
|
||||
{
|
||||
for ($idx = $start; $idx <= $this->fdt['tot_num_glyphs']; ++$idx) {
|
||||
if (isset($this->fdt['indexToLoc'][$idx])) {
|
||||
return $idx;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* build new subset font
|
||||
*
|
||||
* @throws FontException
|
||||
*/
|
||||
protected function buildSubsetFont(): void
|
||||
{
|
||||
$this->subfont = '';
|
||||
$this->subfont .= \pack('N', 0x1_0000); // sfnt version
|
||||
$numTables = \count($this->fdt['table']);
|
||||
$this->subfont .= \pack('n', $numTables); // numTables
|
||||
$entrySelector = \floor(\log($numTables, 2));
|
||||
$searchRange = (2 ** $entrySelector) * 16;
|
||||
$rangeShift = ($numTables * 16) - $searchRange;
|
||||
$this->subfont .= \pack('n', $searchRange); // searchRange
|
||||
$this->subfont .= \pack('n', $entrySelector); // entrySelector
|
||||
$this->subfont .= \pack('n', $rangeShift); // rangeShift
|
||||
// Table offsets stored in $this->fdt start after the 12-byte sfnt header.
|
||||
// The full output adds the table directory immediately after that header,
|
||||
// so both directory offsets and in-buffer table positions must include this base.
|
||||
$tableDataBaseOffset = $numTables * 16;
|
||||
$this->offset = $tableDataBaseOffset;
|
||||
foreach ($this->fdt['table'] as $tag => $data) {
|
||||
$this->subfont .= $tag; // tag
|
||||
$this->subfont .= \pack('N', $data['checkSum']); // checkSum
|
||||
$this->subfont .= \pack('N', $data['offset'] + $this->offset); // offset
|
||||
$this->subfont .= \pack('N', $data['length']); // length
|
||||
}
|
||||
|
||||
foreach ($this->fdt['table'] as $data) {
|
||||
$this->subfont .= $data['data'];
|
||||
}
|
||||
|
||||
// set checkSumAdjustment on head table
|
||||
$checkSumAdjustment = 0xB1B0_AFBA - $this->getTableChecksum($this->subfont, \strlen($this->subfont));
|
||||
$headAdjustmentPos = $tableDataBaseOffset + $this->fdt['table']['head']['offset'] + 8;
|
||||
$this->subfont =
|
||||
\substr($this->subfont, 0, $headAdjustmentPos)
|
||||
. \pack('N', $checkSumAdjustment)
|
||||
. \substr($this->subfont, $headAdjustmentPos + 4);
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user