Initial commit

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