Feature: Labelprint für Kistenetiketten hinzugefügt

This commit is contained in:
2025-10-27 12:14:44 +01:00
parent 43bc416554
commit 14bae6c9ef
1068 changed files with 229014 additions and 1807 deletions

View File

@@ -0,0 +1,298 @@
<?php
/**
* Buffer.php
*
* @since 2011-05-23
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2024 Nicola Asuni - Tecnick.com LTD
* @license http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
* @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\Buffer
*
* @since 2011-05-23
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2024 Nicola Asuni - Tecnick.com LTD
* @license http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
* @link https://github.com/tecnickcom/tc-lib-pdf-font
*
* @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;
/**
* 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 = [];
/**
* Initialize fonts buffer
*
* @param float $kunit Unit of measure conversion ratio.
* @param bool $subset If true embedd only a subset of the fonts
* (stores only the information related to
* the used characters); If false embedd
* full font; This option is valid only for
* TrueTypeUnicode fonts and it 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 otherwhise.
* @param bool $pdfa True if we are in PDF/A mode.
*
* @return string Font key
*/
public function __construct(
protected float $kunit,
protected bool $subset = false,
protected bool $unicode = true,
protected bool $pdfa = false
) {
}
/**
* 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
*/
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 it 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
) {
if ($subset === null) {
$subset = $this->subset;
}
$fobj = new Font($font, $style, $ifile, $subset, $this->unicode, $this->pdfa);
$key = $fobj->getFontkey();
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;
return $key;
}
/**
* Set font file and subset
*
* @param string $key Font key
*/
protected function setFontFile(string $key): void
{
if (empty($this->font[$key]['file'])) {
return;
}
$file = $this->font[$key]['file'];
if (! isset($this->file[$file])) {
$this->file[$file] = [
'dir' => '',
'keys' => [],
'length1' => 0,
'length2' => 0,
'subset' => false,
];
}
if (! in_array($key, $this->file[$file]['keys'])) {
$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'];
$this->file[$file]['subset'] = ($this->file[$file]['subset'] && $this->font[$key]['subset']);
}
/**
* Set font diff
*
* @param string $key Font key
*/
protected function setFontDiff(string $key): void
{
if (empty($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;
}
}

View File

@@ -0,0 +1,60 @@
<?php
/**
* Core.php
*
* @since 2011-05-23
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2024 Nicola Asuni - Tecnick.com LTD
* @license http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
* @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-2024 Nicola Asuni - Tecnick.com LTD
* @license http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
* @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',
];
}

View File

@@ -0,0 +1,34 @@
<?php
/**
* Exception.php
*
* @since 2011-05-23
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2024 Nicola Asuni - Tecnick.com LTD
* @license http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
* @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-2024 Nicola Asuni - Tecnick.com LTD
* @license http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
* @link https://github.com/tecnickcom/tc-lib-pdf-font
*/
class Exception extends \Exception
{
}

View File

@@ -0,0 +1,207 @@
<?php
/**
* Font.php
*
* @since 2011-05-23
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2024 Nicola Asuni - Tecnick.com LTD
* @license http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
* @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\File\File;
/**
* Com\Tecnick\Pdf\Font\Font
*
* @since 2011-05-23
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2024 Nicola Asuni - Tecnick.com LTD
* @license http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
* @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 embedd only a subset of the font
* (stores only the information related to
* the used characters); If false embedd
* full font; This option is valid only for
* TrueTypeUnicode fonts and it 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 if we are in Unicode mode, False otherwhise.
* @param bool $pdfa True if we are in PDF/A mode.
* @param bool $compress Set to false to disable stream compression.
*
* @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
) {
if ($font === '') {
throw new FontException('empty font family name');
}
if (FILE::hasDoubleDots($ifile) || FILE::hasForbiddenProtocol($ifile)) {
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, 255, 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'] = (string) $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;
}
}

View File

@@ -0,0 +1,598 @@
<?php
/**
* Import.php
*
* @since 2011-05-23
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2024 Nicola Asuni - Tecnick.com LTD
* @license http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
* @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\File;
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-2024 Nicola Asuni - Tecnick.com LTD
* @license http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
* @link https://github.com/tecnickcom/tc-lib-pdf-font
*
* @phpstan-import-type TFontData from Load
*
* @SuppressWarnings("PHPMD.ExcessiveClassComplexity")
*/
class Import
{
/**
* 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' => [],
'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 $type Font type. 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.
*
* @throws FontException in case of error
*/
public function __construct(
string $file,
string $output_path = '',
string $type = '',
string $encoding = '',
int $flags = 32,
int $platform_id = 3,
int $encoding_id = 1,
bool $linked = false
) {
if (FILE::hasDoubleDots($file) || FILE::hasForbiddenProtocol($file)) {
throw new FontException('Invalid font file name: ' . $file);
}
$this->fdt['input_file'] = $file;
$this->fdt['file_name'] = $this->makeFontName($file);
if (empty($this->fdt['file_name'])) {
throw new FontException('the font name is empty');
}
$this->fdt['dir'] = $this->findOutputPath($output_path);
$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 = @file_get_contents($file)) === false) {
throw new FontException('unable to read the input font file: ' . $file);
}
$this->font = $font;
$this->fbyte = new Byte($this->font);
$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($this->font, $this->fdt),
'Type1' => new TypeOne($this->font, $this->fdt),
default => new TrueType($this->font, $this->fdt, $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;
}
}
/**
* Save the eported metadata font file
*
* @SuppressWarnings("PHPMD.CyclomaticComplexity")
* @SuppressWarnings("PHPMD.NPathComplexity")
*/
protected function saveFontData(): void
{
$pfile = '{"type":"' . $this->fdt['type'] . '"'
. ',"name":"' . $this->fdt['name'] . '"'
. ',"up":' . $this->fdt['underlinePosition']
. ',"ut":' . $this->fdt['underlineThickness']
. ',"dw":' . (($this->fdt['MissingWidth'] > 0) ? $this->fdt['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('', 131072, "\x00"); // (256 * 256 * 2) = 131072
foreach ($this->fdt['ctgdata'] as $cid => $gid) {
$cidtogidmap = $this->updateCIDtoGIDmap($cidtogidmap, (int) $cid, (int) $gid);
}
// store compressed CIDToGIDMap
$file = new File();
$fpt = $file->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":' . $this->fdt['MissingWidth']
. '}';
if (! empty($this->fdt['cbbox'])) {
$ccboxstr = '';
foreach ($this->fdt['cbbox'] as $cid => $bbox) {
$ccboxstr .= ',"' . $cid . '":[' . $bbox[0] . ',' . $bbox[1] . ',' . $bbox[2] . ',' . $bbox[3] . ']';
}
$pfile .= ',"cbbox":{' . substr($ccboxstr, 1) . '}';
}
if (! empty($this->fdt['cw'])) {
$cwstr = '';
foreach ($this->fdt['cw'] as $cid => $width) {
$cwstr .= ',"' . $cid . '":' . $width;
}
$pfile .= ',"cw":{' . substr($cwstr, 1) . '}';
}
$pfile .= '}' . "\n";
// store file
$file = new File();
$fpt = $file->fopenLocal($this->fdt['datafile'], 'wb');
fwrite($fpt, $pfile);
fclose($fpt);
}
/**
* Make the output font name
*
* @param string $font_file Input font file
*/
protected function makeFontName(string $font_file): string
{
$font_path_parts = pathinfo($font_file);
if (empty($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 !== ''
&& (strpos($output_path, '://') === false)
&& !FILE::hasDoubleDots($output_path)
&& is_writable($output_path)
) {
return $output_path;
}
if (defined('K_PATH_FONTS') && is_writable(K_PATH_FONTS)) {
return K_PATH_FONTS;
}
$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.
*/
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) == 0x10000) {
return 'TrueTypeUnicode';
}
return 'Type1';
}
if (str_starts_with($font_type, 'CID0')) {
return 'cidfont0';
}
if (in_array($font_type, ['Core', 'Type1', 'TrueType', 'TrueTypeUnicode'])) {
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.
*/
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'))
&& (! empty($this->fdt['enc'])
&& (is_string($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) {
if ($enc_target[$idx] != $enc_ref[$idx]) {
if ($idx != $last + 1) {
$diff .= $idx . ' ';
}
$last = $idx;
$diff .= '/' . $enc_target[$idx] . ' ';
}
}
}
return $diff;
}
/**
* Update the CIDToGIDMap string with a new value
*
* @param string $map CIDToGIDMap.
* @param int $cid CID value.
* @param int $gid GID value.
*/
protected function updateCIDtoGIDmap(string $map, int $cid, int $gid): string
{
if (($cid >= 0) && ($cid <= 0xFFFF) && ($gid >= 0)) {
if ($gid > 0xFFFF) {
$gid -= 0x10000;
}
$map[($cid * 2)] = chr($gid >> 8);
$map[(($cid * 2) + 1)] = chr($gid & 0xFF);
}
return $map;
}
}

View File

@@ -0,0 +1,227 @@
<?php
/**
* Core.php
*
* @since 2011-05-23
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2024 Nicola Asuni - Tecnick.com LTD
* @license http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
* @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;
use Com\Tecnick\Pdf\Font\Exception as FontException;
/**
* Com\Tecnick\Pdf\Font\Import\Core
*
* @since 2011-05-23
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2024 Nicola Asuni - Tecnick.com LTD
* @license http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
* @link https://github.com/tecnickcom/tc-lib-pdf-font
*
* @phpstan-import-type TFontData from \Com\Tecnick\Pdf\Font\Load
*/
class Core
{
/**
* @param string $font Content of the input font file
* @param TFontData $fdt Extracted font metrics
*
* @throws FontException in case of error
*/
public function __construct(
protected string $font,
protected array $fdt
) {
$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 (! empty($cwidths[32])) {
$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'];
}
}
$this->fdt['AvgWidth'] = (int) round($this->fdt['AvgWidth'] / count($cwidths));
}
/**
* Extract Metrics
*/
protected function extractMetrics(): void
{
$cwd = [];
$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->setCharWidths($cwd);
}
/**
* Extract Metrics
*
* @param array<int, string> $col Array containing row elements to process
* @param array<int, int> $cwd Array contianing 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':
$cid = (int) $col[1];
if ($cid >= 0) {
$cwd[$cid] = (int) $col[4];
if (! empty($col[14])) {
$this->fdt['cbbox'][$cid] = [(int) $col[10], (int) $col[11], (int) $col[12], (int) $col[13]];
}
}
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
*/
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']);
if (empty($this->fdt['XHeight'])) {
$this->fdt['XHeight'] = 0;
}
}
protected function setMissingValues(): void
{
$this->fdt['Descender'] = $this->fdt['FontBBox'][1];
$this->fdt['Ascender'] = $this->fdt['FontBBox'][3];
if (empty($this->fdt['CapHeight'])) {
$this->fdt['CapHeight'] = $this->fdt['Ascender'];
}
}
/**
* Process Core font
*/
protected function process(): void
{
$this->extractMetrics();
$this->setFlags();
$this->setMissingValues();
$this->remapValues();
}
}

View File

@@ -0,0 +1,811 @@
<?php
/**
* TrueType.php
*
* @since 2011-05-23
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2024 Nicola Asuni - Tecnick.com LTD
* @license http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
* @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\Byte;
use Com\Tecnick\File\File;
use Com\Tecnick\Pdf\Font\Exception as FontException;
use Com\Tecnick\Unicode\Data\Encoding;
/**
* Com\Tecnick\Pdf\Font\Import\TrueType
*
* @since 2011-05-23
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2024 Nicola Asuni - Tecnick.com LTD
* @license http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
* @link https://github.com/tecnickcom/tc-lib-pdf-font
*
* @phpstan-import-type TFontData from \Com\Tecnick\Pdf\Font\Load
*
* @SuppressWarnings("PHPMD.ExcessiveClassComplexity")
* @SuppressWarnings("PHPMD.ExcessiveClassLength")
*/
class TrueType
{
/**
* Array containing subset chars
*
* @var array<int, bool>
*/
protected array $subchars = [];
/**
* Array containing subset glyphs indexes of chars from cmap table
*
* @var array<int, bool>
*/
protected array $subglyphs = [
0 => true,
];
/**
* Pointer position on the original font data
*/
protected int $offset = 0;
/**
* Process TrueType font
*
* @param string $font Content of the input font file
* @param TFontData $fdt Extracted font metrics
* @param Byte $fbyte Object used to read font bytes
* @param array<int, bool> $subchars Array containing subset chars
*
* @throws FontException in case of error
*/
public function __construct(
protected string $font,
protected array $fdt,
protected Byte $fbyte,
array $subchars = []
) {
ksort($subchars);
$this->subchars = $subchars;
$this->process();
}
/**
* Get all the extracted font metrics
*
* @return TFontData
*/
public function getFontMetrics(): array
{
return $this->fdt;
}
/**
* Get glyphs in the subset
*
* @return array<int, bool>
*/
public function getSubGlyphs(): array
{
return $this->subglyphs;
}
/**
* Process TrueType font
*/
protected function process(): void
{
$this->isValidType();
$this->setFontFile();
$this->getTables();
$this->checkMagickNumber();
$this->offset += 2; // skip flags
$this->getBbox();
$this->getIndexToLoc();
$this->getEncodingTables();
$this->getOS2Metrics();
$this->getFontName();
$this->getPostData();
$this->getHheaData();
$this->getMaxpData();
$this->getCIDToGIDMap();
$this->getHeights();
$this->getWidths();
}
/**
* Check if the font is a valid type
*
* @throws FontException if the font is invalid
*/
protected function isValidType(): void
{
if ($this->fbyte->getULong($this->offset) != 0x10000) {
throw new FontException('sfnt version must be 0x00010000 for TrueType version 1.0.');
}
$this->offset += 4;
}
/**
* Copy or link the original font file
*/
protected function setFontFile(): void
{
if (! empty($this->fdt['desc']['MaxWidth'])) {
// subsetting mode
$this->fdt['Flags'] = $this->fdt['desc']['Flags'];
return;
}
if ($this->fdt['type'] == 'cidfont0') {
return;
}
if ($this->fdt['linked']) {
// creates a symbolic link to the existing font
symlink($this->fdt['input_file'], $this->fdt['dir'] . $this->fdt['file_name']);
return;
}
// store compressed font
$this->fdt['file'] = $this->fdt['file_name'] . '.z';
$file = new File();
$fpt = $file->fopenLocal($this->fdt['dir'] . $this->fdt['file'], 'wb');
$cmpr = gzcompress($this->font);
if ($cmpr === false) {
throw new FontException('Error compressing font file.');
}
fwrite($fpt, $cmpr);
fclose($fpt);
}
/**
* Get the font tables
*/
protected function getTables(): void
{
// get number of tables
$numTables = $this->fbyte->getUShort($this->offset);
$this->offset += 2;
// skip searchRange, entrySelector and rangeShift
$this->offset += 6;
// tables array
$this->fdt['table'] = [];
// ---------- get tables ----------
for ($idx = 0; $idx < $numTables; ++$idx) {
// get table info
$tag = substr($this->font, $this->offset, 4);
$this->offset += 4;
$this->fdt['table'][$tag] = [
'checkSum' => 0,
'data' => '',
'length' => 0,
'offset' => 0,
];
$this->fdt['table'][$tag]['checkSum'] = $this->fbyte->getULong($this->offset);
$this->offset += 4;
$this->fdt['table'][$tag]['offset'] = $this->fbyte->getULong($this->offset);
$this->offset += 4;
$this->fdt['table'][$tag]['length'] = $this->fbyte->getULong($this->offset);
$this->offset += 4;
}
}
/**
* Check if the font is a valid type
*
* @throws FontException if the font is invalid
*/
protected function checkMagickNumber(): void
{
$this->offset = ($this->fdt['table']['head']['offset'] + 12);
if ($this->fbyte->getULong($this->offset) != 0x5F0F3CF5) {
// magicNumber must be 0x5F0F3CF5
throw new FontException('magicNumber must be 0x5F0F3CF5');
}
$this->offset += 4;
}
/**
* Get BBox, units and flags
*/
protected function getBbox(): void
{
$this->fdt['unitsPerEm'] = $this->fbyte->getUShort($this->offset);
$this->offset += 2;
// units ratio constant
$this->fdt['urk'] = (1000 / $this->fdt['unitsPerEm']);
$this->offset += 16; // skip created, modified
$xMin = (int) round($this->fbyte->getFWord($this->offset) * $this->fdt['urk']);
$this->offset += 2;
$yMin = (int) round($this->fbyte->getFWord($this->offset) * $this->fdt['urk']);
$this->offset += 2;
$xMax = (int) round($this->fbyte->getFWord($this->offset) * $this->fdt['urk']);
$this->offset += 2;
$yMax = (int) round($this->fbyte->getFWord($this->offset) * $this->fdt['urk']);
$this->offset += 2;
$this->fdt['bbox'] = $xMin . ' ' . $yMin . ' ' . $xMax . ' ' . $yMax;
$macStyle = $this->fbyte->getUShort($this->offset);
$this->offset += 2;
// PDF font flags
if (($macStyle & 2) == 2) {
// italic flag
$this->fdt['Flags'] |= 64;
}
}
/**
* Get index to loc map
*/
protected function getIndexToLoc(): void
{
// get offset mode (indexToLocFormat : 0 = short, 1 = long)
$this->offset = ($this->fdt['table']['head']['offset'] + 50);
$this->fdt['short_offset'] = ($this->fbyte->getShort($this->offset) == 0);
$this->offset += 2;
// get the offsets to the locations of the glyphs in the font, relative to the beginning of the glyphData table
$this->fdt['indexToLoc'] = [];
$this->offset = $this->fdt['table']['loca']['offset'];
if ($this->fdt['short_offset']) {
// short version
$this->fdt['tot_num_glyphs'] = (int) floor($this->fdt['table']['loca']['length'] / 2); // numGlyphs + 1
for ($idx = 0; $idx < $this->fdt['tot_num_glyphs']; ++$idx) {
$this->fdt['indexToLoc'][$idx] = $this->fbyte->getUShort($this->offset) * 2;
if (
isset($this->fdt['indexToLoc'][($idx - 1)])
&& ($this->fdt['indexToLoc'][$idx] === $this->fdt['indexToLoc'][($idx - 1)])
) {
// the last glyph didn't have an outline
unset($this->fdt['indexToLoc'][($idx - 1)]);
}
$this->offset += 2;
}
} else {
// long version
$this->fdt['tot_num_glyphs'] = (int) floor($this->fdt['table']['loca']['length'] / 4); // numGlyphs + 1
for ($idx = 0; $idx < $this->fdt['tot_num_glyphs']; ++$idx) {
$this->fdt['indexToLoc'][$idx] = $this->fbyte->getULong($this->offset);
if (
isset($this->fdt['indexToLoc'][($idx - 1)])
&& ($this->fdt['indexToLoc'][$idx] === $this->fdt['indexToLoc'][($idx - 1)])
) {
// the last glyph didn't have an outline
unset($this->fdt['indexToLoc'][($idx - 1)]);
}
$this->offset += 4;
}
}
}
protected function getEncodingTables(): void
{
// get glyphs indexes of chars from cmap table
$this->offset = $this->fdt['table']['cmap']['offset'] + 2;
$numEncodingTables = $this->fbyte->getUShort($this->offset);
$this->offset += 2;
$this->fdt['encodingTables'] = [];
for ($idx = 0; $idx < $numEncodingTables; ++$idx) {
$this->fdt['encodingTables'][$idx]['platformID'] = $this->fbyte->getUShort($this->offset);
$this->offset += 2;
$this->fdt['encodingTables'][$idx]['encodingID'] = $this->fbyte->getUShort($this->offset);
$this->offset += 2;
$this->fdt['encodingTables'][$idx]['offset'] = $this->fbyte->getULong($this->offset);
$this->offset += 4;
}
}
/**
* Get encoding tables
*/
protected function getOS2Metrics(): void
{
$this->offset = $this->fdt['table']['OS/2']['offset'];
$this->offset += 2; // skip version
// xAvgCharWidth
$this->fdt['AvgWidth'] = (int) round($this->fbyte->getFWord($this->offset) * $this->fdt['urk']);
$this->offset += 2;
// usWeightClass
$usWeightClass = round($this->fbyte->getUFWord($this->offset) * $this->fdt['urk']);
// estimate StemV and StemH (400 = usWeightClass for Normal - Regular font)
$this->fdt['StemV'] = (int) round((70 * $usWeightClass) / 400);
$this->fdt['StemH'] = (int) round((30 * $usWeightClass) / 400);
$this->offset += 2;
$this->offset += 2; // usWidthClass
$fsType = $this->fbyte->getShort($this->offset);
$this->offset += 2;
if ($fsType == 2) {
throw new FontException(
'This Font cannot be modified, embedded or exchanged in any manner'
. ' without first obtaining permission of the legal owner.'
);
}
}
protected function getFontName(): void
{
$this->fdt['name'] = '';
$this->offset = $this->fdt['table']['name']['offset'];
$this->offset += 2; // skip Format selector (=0).
// Number of NameRecords that follow n.
$numNameRecords = $this->fbyte->getUShort($this->offset);
$this->offset += 2;
// Offset to start of string storage (from start of table).
$stringStorageOffset = $this->fbyte->getUShort($this->offset);
$this->offset += 2;
for ($idx = 0; $idx < $numNameRecords; ++$idx) {
$this->offset += 6; // skip Platform ID, Platform-specific encoding ID, Language ID.
// Name ID.
$nameID = $this->fbyte->getUShort($this->offset);
$this->offset += 2;
if ($nameID == 6) {
// String length (in bytes).
$stringLength = $this->fbyte->getUShort($this->offset);
$this->offset += 2;
// String offset from start of storage area (in bytes).
$stringOffset = $this->fbyte->getUShort($this->offset);
$this->offset += 2;
$this->offset = ($this->fdt['table']['name']['offset'] + $stringStorageOffset + $stringOffset);
$this->fdt['name'] = substr($this->font, $this->offset, $stringLength);
$name = preg_replace('/[^a-zA-Z0-9_\-]/', '', $this->fdt['name']);
if (($name === null) || ($name === '')) {
throw new FontException('Error getting font name.');
}
$this->fdt['name'] = $name;
break;
} else {
$this->offset += 4; // skip String length, String offset
}
}
}
protected function getPostData(): void
{
$this->offset = $this->fdt['table']['post']['offset'];
$this->offset += 4; // skip Format Type
$this->fdt['italicAngle'] = $this->fbyte->getFixed($this->offset);
$this->offset += 4;
$this->fdt['underlinePosition'] = (int) round($this->fbyte->getFWord($this->offset) * $this->fdt['urk']);
$this->offset += 2;
$this->fdt['underlineThickness'] = (int) round($this->fbyte->getFWord($this->offset) * $this->fdt['urk']);
$this->offset += 2;
$isFixedPitch = ($this->fbyte->getULong($this->offset) != 0);
$this->offset += 2;
if ($isFixedPitch) {
$this->fdt['Flags'] |= 1;
}
}
protected function getHheaData(): void
{
// ---------- get hhea data ----------
$this->offset = $this->fdt['table']['hhea']['offset'];
$this->offset += 4; // skip Table version number
// Ascender
$this->fdt['Ascent'] = (int) round($this->fbyte->getFWord($this->offset) * $this->fdt['urk']);
$this->offset += 2;
// Descender
$this->fdt['Descent'] = (int) round($this->fbyte->getFWord($this->offset) * $this->fdt['urk']);
$this->offset += 2;
// LineGap
$this->fdt['Leading'] = (int) round($this->fbyte->getFWord($this->offset) * $this->fdt['urk']);
$this->offset += 2;
// advanceWidthMax
$this->fdt['MaxWidth'] = (int) round($this->fbyte->getUFWord($this->offset) * $this->fdt['urk']);
$this->offset += 2;
$this->offset += 22; // skip some values
// get the number of hMetric entries in hmtx table
$this->fdt['numHMetrics'] = $this->fbyte->getUShort($this->offset);
}
protected function getMaxpData(): void
{
$this->offset = $this->fdt['table']['maxp']['offset'];
$this->offset += 4; // skip Table version number
// get the the number of glyphs in the font.
$this->fdt['numGlyphs'] = $this->fbyte->getUShort($this->offset);
}
/**
* Get font heights
*/
protected function getHeights(): void
{
// get xHeight (height of x)
$this->fdt['XHeight'] = ($this->fdt['Ascent'] + $this->fdt['Descent']);
if (! empty($this->fdt['ctgdata'][120])) {
$this->offset = (
$this->fdt['table']['glyf']['offset']
+ $this->fdt['indexToLoc'][$this->fdt['ctgdata'][120]]
+ 4
);
$yMin = $this->fbyte->getFWord($this->offset);
$this->offset += 4;
$yMax = $this->fbyte->getFWord($this->offset);
$this->offset += 2;
$this->fdt['XHeight'] = (int) round(($yMax - $yMin) * $this->fdt['urk']);
}
// get CapHeight (height of H)
$this->fdt['CapHeight'] = (int) $this->fdt['Ascent'];
if (! empty($this->fdt['ctgdata'][72])) {
$this->offset = (
$this->fdt['table']['glyf']['offset']
+ $this->fdt['indexToLoc'][$this->fdt['ctgdata'][72]]
+ 4
);
$yMin = $this->fbyte->getFWord($this->offset);
$this->offset += 4;
$yMax = $this->fbyte->getFWord($this->offset);
$this->offset += 2;
$this->fdt['CapHeight'] = (int) round(($yMax - $yMin) * $this->fdt['urk']);
}
}
/**
* Get font widths
*/
protected function getWidths(): void
{
// create widths array
$chw = [];
$this->offset = $this->fdt['table']['hmtx']['offset'];
for ($i = 0; $i < $this->fdt['numHMetrics']; ++$i) {
$chw[$i] = round($this->fbyte->getUFWord($this->offset) * $this->fdt['urk']);
$this->offset += 4; // skip lsb
}
if ($this->fdt['numHMetrics'] < $this->fdt['numGlyphs']) {
// fill missing widths with the last value
$chw = array_pad($chw, $this->fdt['numGlyphs'], $chw[($this->fdt['numHMetrics'] - 1)]);
}
$this->fdt['MissingWidth'] = $chw[0];
$this->fdt['cw'] = [];
$this->fdt['cbbox'] = [];
for ($cid = 0; $cid <= 65535; ++$cid) {
if (isset($this->fdt['ctgdata'][$cid])) {
if (isset($chw[$this->fdt['ctgdata'][$cid]])) {
$this->fdt['cw'][$cid] = $chw[$this->fdt['ctgdata'][$cid]];
}
if (isset($this->fdt['indexToLoc'][$this->fdt['ctgdata'][$cid]])) {
$this->offset = (
$this->fdt['table']['glyf']['offset']
+ $this->fdt['indexToLoc'][$this->fdt['ctgdata'][$cid]]
);
$xMin = (int) round($this->fbyte->getFWord($this->offset + 2) * $this->fdt['urk']);
$yMin = (int) round($this->fbyte->getFWord($this->offset + 4) * $this->fdt['urk']);
$xMax = (int) round($this->fbyte->getFWord($this->offset + 6) * $this->fdt['urk']);
$yMax = (int) round($this->fbyte->getFWord($this->offset + 8) * $this->fdt['urk']);
$this->fdt['cbbox'][$cid] = [$xMin, $yMin, $xMax, $yMax];
}
}
}
}
/**
* Add CTG entry
*/
protected function addCtgItem(int $cid, int $gid): void
{
$this->fdt['ctgdata'][$cid] = $gid;
if (isset($this->subchars[$cid])) {
$this->subglyphs[$gid] = true;
}
}
/**
* Process the CID To GID Map.
*
* @SuppressWarnings("PHPMD.CyclomaticComplexity")
*/
protected function getCIDToGIDMap(): void
{
$this->fdt['ctgdata'] = [];
foreach ($this->fdt['encodingTables'] as $enctable) {
// get only specified Platform ID and Encoding ID
if (
($enctable['platformID'] == $this->fdt['platform_id'])
&& ($enctable['encodingID'] == $this->fdt['encoding_id'])
) {
$this->offset = ($this->fdt['table']['cmap']['offset'] + $enctable['offset']);
$format = $this->fbyte->getUShort($this->offset);
$this->offset += 2;
match ($format) {
0 => $this->processFormat0(),
2 => $this->processFormat2(),
4 => $this->processFormat4(),
6 => $this->processFormat6(),
8 => $this->processFormat8(),
10 => $this->processFormat10(),
12 => $this->processFormat12(),
13 => $this->processFormat13(),
14 => $this->processFormat14(),
default => throw new FontException('Unsupported cmap format: ' . $format),
};
}
}
if (! isset($this->fdt['ctgdata'][0])) {
$this->fdt['ctgdata'][0] = 0;
}
if ($this->fdt['type'] != 'TrueTypeUnicode') {
return;
}
if (count($this->fdt['ctgdata']) != 256) {
return;
}
$this->fdt['type'] = 'TrueType';
}
/**
* Process Format 0: Byte encoding table
*/
protected function processFormat0(): void
{
$this->offset += 4; // skip length and version/language
for ($chr = 0; $chr < 256; ++$chr) {
$gid = $this->fbyte->getByte($this->offset);
$this->addCtgItem($chr, $gid);
++$this->offset;
}
}
/**
* Process Format 2: High-byte mapping through table
*/
protected function processFormat2(): void
{
$this->offset += 4; // skip length and version/language
$numSubHeaders = 0;
for ($chr = 0; $chr < 256; ++$chr) {
// Array that maps high bytes to subHeaders: value is subHeader index * 8.
$subHeaderKeys[$chr] = ($this->fbyte->getUShort($this->offset) / 8);
$this->offset += 2;
if ($numSubHeaders < $subHeaderKeys[$chr]) {
$numSubHeaders = $subHeaderKeys[$chr];
}
}
// the number of subHeaders is equal to the max of subHeaderKeys + 1
++$numSubHeaders;
// read subHeader structures
$subHeaders = [];
$numGlyphIndexArray = 0;
for ($ish = 0; $ish < $numSubHeaders; ++$ish) {
$subHeaders[$ish]['firstCode'] = $this->fbyte->getUShort($this->offset);
$this->offset += 2;
$subHeaders[$ish]['entryCount'] = $this->fbyte->getUShort($this->offset);
$this->offset += 2;
$subHeaders[$ish]['idDelta'] = $this->fbyte->getUShort($this->offset);
$this->offset += 2;
$subHeaders[$ish]['idRangeOffset'] = $this->fbyte->getUShort($this->offset);
$this->offset += 2;
$subHeaders[$ish]['idRangeOffset'] -= (2 + (($numSubHeaders - $ish - 1) * 8));
$subHeaders[$ish]['idRangeOffset'] /= 2;
$numGlyphIndexArray += $subHeaders[$ish]['entryCount'];
}
$glyphIndexArray = [
0 => 0,
];
for ($gid = 0; $gid < $numGlyphIndexArray; ++$gid) {
$glyphIndexArray[$gid] = $this->fbyte->getUShort($this->offset);
$this->offset += 2;
}
for ($chr = 0; $chr < 256; ++$chr) {
$shk = $subHeaderKeys[$chr];
if ($shk == 0) {
// one byte code
$cdx = $chr;
$gid = $glyphIndexArray[0];
$this->addCtgItem($cdx, $gid);
} else {
// two bytes code
$start_byte = $subHeaders[$shk]['firstCode'];
$end_byte = $start_byte + $subHeaders[$shk]['entryCount'];
for ($jdx = $start_byte; $jdx < $end_byte; ++$jdx) {
// combine high and low bytes
$cdx = (($chr << 8) + $jdx);
$idRangeOffset = ($subHeaders[$shk]['idRangeOffset'] + $jdx - $subHeaders[$shk]['firstCode']);
$gid = max(0, (($glyphIndexArray[$idRangeOffset] + $subHeaders[$shk]['idDelta']) % 65536));
$this->addCtgItem($cdx, $gid);
}
}
}
}
/**
* Process Format 4: Segment mapping to delta values
*/
protected function processFormat4(): void
{
$length = $this->fbyte->getUShort($this->offset);
$this->offset += 2;
$this->offset += 2; // skip version/language
$segCount = floor($this->fbyte->getUShort($this->offset) / 2);
$this->offset += 2;
$this->offset += 6; // skip searchRange, entrySelector, rangeShift
$endCount = []; // array of end character codes for each segment
for ($kdx = 0; $kdx < $segCount; ++$kdx) {
$endCount[$kdx] = $this->fbyte->getUShort($this->offset);
$this->offset += 2;
}
$this->offset += 2; // skip reservedPad
$startCount = []; // array of start character codes for each segment
for ($kdx = 0; $kdx < $segCount; ++$kdx) {
$startCount[$kdx] = $this->fbyte->getUShort($this->offset);
$this->offset += 2;
}
$idDelta = []; // delta for all character codes in segment
for ($kdx = 0; $kdx < $segCount; ++$kdx) {
$idDelta[$kdx] = $this->fbyte->getUShort($this->offset);
$this->offset += 2;
}
$idRangeOffset = []; // Offsets into glyphIdArray or 0
for ($kdx = 0; $kdx < $segCount; ++$kdx) {
$idRangeOffset[$kdx] = $this->fbyte->getUShort($this->offset);
$this->offset += 2;
}
$gidlen = (floor($length / 2) - 8 - (4 * $segCount));
$glyphIdArray = []; // glyph index array
for ($kdx = 0; $kdx < $gidlen; ++$kdx) {
$glyphIdArray[$kdx] = $this->fbyte->getUShort($this->offset);
$this->offset += 2;
}
for ($kdx = 0; $kdx < $segCount; ++$kdx) {
for ($chr = $startCount[$kdx]; $chr <= $endCount[$kdx]; ++$chr) {
if ($idRangeOffset[$kdx] == 0) {
$gid = max(0, (($idDelta[$kdx] + $chr) % 65536));
} else {
$gid = (floor($idRangeOffset[$kdx] / 2) + ($chr - $startCount[$kdx]) - ($segCount - $kdx));
$gid = max(0, (($glyphIdArray[$gid] + $idDelta[$kdx]) % 65536));
}
$this->addCtgItem($chr, $gid);
}
}
}
/**
* Process Format 6: Trimmed table mapping
*/
protected function processFormat6(): void
{
$this->offset += 4; // skip length and version/language
$firstCode = $this->fbyte->getUShort($this->offset);
$this->offset += 2;
$entryCount = $this->fbyte->getUShort($this->offset);
$this->offset += 2;
for ($kdx = 0; $kdx < $entryCount; ++$kdx) {
$chr = ($kdx + $firstCode);
$gid = $this->fbyte->getUShort($this->offset);
$this->offset += 2;
$this->addCtgItem($chr, $gid);
}
}
/**
* Process Format 8: Mixed 16-bit and 32-bit coverage
*/
protected function processFormat8(): void
{
$this->offset += 10; // skip reserved, length and version/language
for ($kdx = 0; $kdx < 8192; ++$kdx) {
$is32[$kdx] = $this->fbyte->getByte($this->offset);
++$this->offset;
}
$nGroups = $this->fbyte->getULong($this->offset);
$this->offset += 4;
for ($idx = 0; $idx < $nGroups; ++$idx) {
$startCharCode = $this->fbyte->getULong($this->offset);
$this->offset += 4;
$endCharCode = $this->fbyte->getULong($this->offset);
$this->offset += 4;
$startGlyphID = $this->fbyte->getULong($this->offset);
$this->offset += 4;
for ($cpw = $startCharCode; $cpw <= $endCharCode; ++$cpw) {
$is32idx = floor($cpw / 8);
if ((isset($is32[$is32idx])) && (($is32[$is32idx] & (1 << (7 - ($cpw % 8)))) == 0)) {
$chr = $cpw;
} else {
// 32 bit format
// convert to decimal (http://www.unicode.org/faq//utf_bom.html#utf16-4)
//LEAD_OFFSET = (0xD800 - (0x10000 >> 10)) = 55232
//SURROGATE_OFFSET = (0x10000 - (0xD800 << 10) - 0xDC00) = -56613888
$chr = (((55232 + ($cpw >> 10)) << 10) + (0xDC00 + ($cpw & 0x3FF)) - 56_613_888);
}
$this->addCtgItem($chr, $startGlyphID);
$this->fdt['ctgdata'][$chr] = 0; // overwrite
++$startGlyphID;
}
}
}
/**
* Process Format 10: Trimmed array
*/
protected function processFormat10(): void
{
$this->offset += 10; // skip reserved, length and version/language
$startCharCode = $this->fbyte->getULong($this->offset);
$this->offset += 4;
$numChars = $this->fbyte->getULong($this->offset);
$this->offset += 4;
for ($kdx = 0; $kdx < $numChars; ++$kdx) {
$chr = ($kdx + $startCharCode);
$gid = $this->fbyte->getUShort($this->offset);
$this->addCtgItem($chr, $gid);
$this->offset += 2;
}
}
/**
* Process Format 12: Segmented coverage
*/
protected function processFormat12(): void
{
$this->offset += 10; // skip length and version/language
$nGroups = $this->fbyte->getULong($this->offset);
$this->offset += 4;
for ($kdx = 0; $kdx < $nGroups; ++$kdx) {
$startCharCode = $this->fbyte->getULong($this->offset);
$this->offset += 4;
$endCharCode = $this->fbyte->getULong($this->offset);
$this->offset += 4;
$startGlyphCode = $this->fbyte->getULong($this->offset);
$this->offset += 4;
for ($chr = $startCharCode; $chr <= $endCharCode; ++$chr) {
$this->addCtgItem($chr, $startGlyphCode);
++$startGlyphCode;
}
}
}
/**
* Process Format 13: Many-to-one range mappings
*
* @TODO: TO BE IMPLEMENTED
*/
protected function processFormat13(): void
{
}
/**
* Process Format 14: Unicode Variation Sequences
*
* @TODO: TO BE IMPLEMENTED
*/
protected function processFormat14(): void
{
}
}

View File

@@ -0,0 +1,360 @@
<?php
/**
* TypeOne.php
*
* @since 2011-05-23
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2024 Nicola Asuni - Tecnick.com LTD
* @license http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
* @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;
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-2024 Nicola Asuni - Tecnick.com LTD
* @license http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
* @link https://github.com/tecnickcom/tc-lib-pdf-font
*
* @SuppressWarnings("PHPMD.ExcessiveClassComplexity")
*/
class TypeOne extends \Com\Tecnick\Pdf\Font\Import\Core
{
/**
* Store font data
*/
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'];
$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'];
$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';
$file = new File();
$fpt = $file->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
*/
protected function extractFontInfo(): void
{
if (preg_match('#/FontName[\s]*+\/([^\s]*+)#', $this->font, $matches) !== 1) {
preg_match('#/FullName[\s]*+\(([^\)]*+)#', $this->font, $matches);
}
$name = preg_replace('/[^a-zA-Z0-9_\-]/', '', $matches[1]);
if ($name === null) {
throw new FontException('Unable to extract font name');
}
$this->fdt['name'] = $name;
preg_match('#/FontBBox[\s]*+{([^}]*+)#', $this->font, $matches);
$rawbvl = explode(' ', trim($matches[1]));
$bvl = [(int) $rawbvl[0], (int) $rawbvl[1], (int) $rawbvl[2], (int) $rawbvl[3]];
$this->fdt['bbox'] = implode(' ', $bvl);
$this->fdt['Ascent'] = $bvl[3];
$this->fdt['Descent'] = $bvl[1];
preg_match('#/ItalicAngle[\s]*+([0-9\+\-]*+)#', $this->font, $matches);
$this->fdt['italicAngle'] = (int) $matches[1];
if ($this->fdt['italicAngle'] != 0) {
$this->fdt['Flags'] |= 64;
}
preg_match('#/UnderlinePosition[\s]*+([0-9\+\-]*+)#', $this->font, $matches);
$this->fdt['underlinePosition'] = (int) $matches[1];
preg_match('#/UnderlineThickness[\s]*+([0-9\+\-]*+)#', $this->font, $matches);
$this->fdt['underlineThickness'] = (int) $matches[1];
preg_match('#/isFixedPitch[\s]*+([^\s]*+)#', $this->font, $matches);
if ($matches[1] == 'true') {
$this->fdt['Flags'] = (((int) $this->fdt['Flags']) | 1);
}
preg_match('#/Weight[\s]*+\(([^\)]*+)#', $this->font, $matches);
if (! empty($matches[1])) {
$this->fdt['weight'] = strtolower($matches[1]);
}
$this->fdt['weight'] = 'Book';
$this->fdt['Leading'] = 0;
}
/**
* Extract Font information
*
* @return array<string, int>
*/
protected function getInternalMap(): array
{
$imap = [];
if (preg_match_all('#dup[\s]([0-9]+)[\s]*+/([^\s]*+)[\s]put#sU', $this->font, $fmap, PREG_SET_ORDER) > 0) {
foreach ($fmap as $val) {
$imap[$val[2]] = (int) $val[1];
}
}
return $imap;
}
/**
* Decrypt eexec encrypted part
*/
protected function getEplain(): string
{
$csr = 55665; // eexec encryption constant
$cc1 = 52845;
$cc2 = 22719;
$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) % 65536);
}
return $eplain;
}
/**
* Extract eexec info
*
* @return array<int, array<int, string>>
*/
protected function extractEplainInfo(): array
{
$eplain = $this->getEplain();
if (preg_match('#/ForceBold[\s]*+([^\s]*+)#', $eplain, $matches) > 0 && $matches[1] == 'true') {
$this->fdt['Flags'] |= 0x40000;
}
$this->extractStem($eplain);
if (preg_match('#/BlueValues[\s]*+\[([^\]]*+)#', $eplain, $matches) > 0) {
$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
{
if (preg_match('#/StdVW[\s]*+\[([^\]]*+)#', $eplain, $matches) > 0) {
$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) > 0 ? (int) $matches[1] : 30;
if (preg_match('#/Cap[X]?Height[\s]*+\[([^\]]*+)#', $eplain, $matches) > 0) {
$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;
if (preg_match('#/lenIV[\s]*+([\d]*+)#', $eplain, $matches) > 0) {
$this->fdt['lenIV'] = (int) $matches[1];
}
}
/**
* @return array<int, array<int, string>>
*/
protected function getCharstringData(string $eplain): array
{
$this->fdt['enc_map'] = [];
$eplain = substr($eplain, (strpos($eplain, '/CharStrings') + 1));
preg_match_all('#/([A-Za-z0-9\.]*+)[\s][0-9]+[\s]RD[\s](.*)[\s]ND#sU', $eplain, $matches, PREG_SET_ORDER);
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]];
}
if ($this->fdt['enc_map'] === false) {
return 0;
}
$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
*/
protected function decodeNumber(
int $idx,
int &$cck,
int &$cid,
array &$ccom,
array &$cdec,
array &$cwidths
): int {
if ($ccom[$idx] == 255) {
$sval = chr($ccom[($idx + 1)]) . chr($ccom[($idx + 2)]) . chr($ccom[($idx + 3)]) . chr($ccom[($idx + 4)]);
$vsval = unpack('li', $sval);
if (($vsval === false) || (!is_numeric($vsval['i']))) {
throw new FontException('Unable to unpack number');
}
$cdec[$cck] = (int) $vsval['i'];
return ($idx + 5);
}
if ($ccom[$idx] >= 251) {
$cdec[$cck] = ((-($ccom[$idx] - 251) * 256) - $ccom[($idx + 1)] - 108);
return ($idx + 2);
}
if ($ccom[$idx] >= 247) {
$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
*/
protected function process(): void
{
$this->storeFontData();
$this->extractFontInfo();
$imap = $this->getInternalMap();
$matches = $this->extractEplainInfo();
$cwidths = [];
$cc1 = 52845;
$cc2 = 22719;
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) % 65536);
}
// 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);
}
}

View File

@@ -0,0 +1,506 @@
<?php
/**
* Load.php
*
* @since 2011-05-23
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2024 Nicola Asuni - Tecnick.com LTD
* @license http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
* @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\Load
*
* @since 2011-05-23
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2024 Nicola Asuni - Tecnick.com LTD
* @license http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
* @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,
* '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>,
* 'datafile': string,
* 'desc': TFontDataDesc,
* 'diff': string,
* 'diff_n': 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
{
/**
* 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' => [],
'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' => '',
];
/**
* 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();
// read the font definition file
if (! @is_readable($this->data['ifile'])) {
throw new FontException('unable to read file: ' . $this->data['ifile']);
}
$fdt = @file_get_contents($this->data['ifile']);
if ($fdt === false) {
throw new FontException('unable to read file: ' . $this->data['ifile']);
}
$fdtdata = @json_decode($fdt, true, 5, JSON_OBJECT_AS_ARRAY);
if ($fdtdata === null) {
throw new FontException('JSON decoding error [' . json_last_error() . ']');
}
if (! is_array($fdtdata) || (! isset($fdtdata['type']))) {
throw new FontException('fhe font definition file has a bad format: ' . $this->data['ifile']);
}
$this->data = array_replace_recursive($this->data, $fdtdata);
}
/**
* Returns a list of font directories
*
* @return array<string> Font directories
*/
protected function findFontDirectories(): array
{
$dir = new Dir();
$dirs = [''];
if (defined('K_PATH_FONTS')) {
$dirs[] = K_PATH_FONTS;
$glb = glob(K_PATH_FONTS . 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);
}
/**
* Load the font data
*
* @throws FontException in case of error
*/
protected function findFontFile(): void
{
if (! empty($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)) {
$this->data['ifile'] = $dir . DIRECTORY_SEPARATOR . $file;
$this->data['dir'] = $dir;
break 2;
}
}
// we haven't found the version with style variations
$this->data['fakestyle'] = true;
}
}
protected function setDefaultWidth(): void
{
if (! empty($this->data['dw'])) {
return;
}
if ($this->data['desc']['MissingWidth'] > 0) {
$this->data['dw'] = $this->data['desc']['MissingWidth'];
} elseif (! empty($this->data['cw'][32])) {
$this->data['dw'] = $this->data['cw'][32];
} else {
$this->data['dw'] = 600;
}
}
/**
* Check Font Type
*/
protected function checkType(): void
{
if (isset(self::FONTTYPES[$this->data['type']])) {
return;
}
throw new FontException('Unknow font type: ' . $this->data['type']);
}
protected function setName(): void
{
if ($this->data['type'] == 'Core') {
$this->data['name'] = (string) Core::FONT[$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 (empty($this->data['name'])) {
$this->data['name'] = (string) $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'] = empty($this->data['desc']['StemV'])
? 123 : (int) round($this->data['desc']['StemV'] * 1.75);
}
// artificial italic
if ($this->data['mode']['italic']) {
$this->data['name'] .= 'Italic';
if (! empty($this->data['desc']['ItalicAngle'])) {
$this->data['desc']['ItalicAngle'] -= 11;
} else {
$this->data['desc']['ItalicAngle'] = -11;
}
if (! empty($this->data['desc']['Flags'])) {
$this->data['desc']['Flags'] |= 64; //bit 7
} else {
$this->data['desc']['Flags'] = 64;
}
}
}
public function setFileData(): void
{
if (empty($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'];
}
}
}

View File

@@ -0,0 +1,411 @@
<?php
/**
* OutFont.php
*
* @since 2011-05-23
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2024 Nicola Asuni - Tecnick.com LTD
* @license http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
* @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\Encrypt\Encrypt;
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-2024 Nicola Asuni - Tecnick.com LTD
* @license http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
* @link https://github.com/tecnickcom/tc-lib-pdf-font
*
* @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;
/**
* 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 array{
* 'cidinfo': TFontDataCidInfo,
* 'cw': array<int, int>,
* 'desc': TFontDataDesc,
* 'dw': int,
* 'enc': string,
* 'i': int,
* 'n': int,
* 'name': string,
* 'subset': bool,
* 'subsetchars': array<int, bool>,
* } $font Font to process
*
* return string
*/
protected function getCid0(array $font): string
{
$cidoffset = 0;
if (! isset($font['cw'][1])) {
$cidoffset = 31;
}
$this->uniToCid($font, $cidoffset);
$name = $font['name'];
$longname = $name;
if (! empty($font['enc'])) {
$longname .= '-' . $font['enc'];
}
// obj 1
$out = $font['n'] . ' 0 obj' . "\n"
. '<</Type /Font'
. ' /Subtype /Type0'
. ' /BaseFont /' . $longname
. ' /Name /F' . $font['i'];
if (! empty($font['enc'])) {
$out .= ' /Encoding /' . $font['enc'];
}
$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($font['cidinfo']['Registry'], $this->pon)
. ' /Ordering ' . $this->enc->escapeDataString($font['cidinfo']['Ordering'], $this->pon)
. ' /Supplement ' . $font['cidinfo']['Supplement'];
$out .= ' /CIDSystemInfo <<' . $cidinfo . '>>'
. ' /FontDescriptor ' . ($this->pon + 1) . ' 0 R'
. ' /DW ' . $font['dw'] . "\n"
. $this->getCharWidths($font, $cidoffset)
. ' >>' . "\n"
. 'endobj' . "\n";
// obj 3
$out .= (++$this->pon) . ' 0 obj' . "\n"
. '<</Type /FontDescriptor /FontName /' . $name;
foreach ($font['desc'] as $key => $val) {
if ($key !== 'Style') { // @phpstan-ignore-line
$out .= $this->getKeyValOut($key, $val);
}
}
return $out . ('>>' . "\n"
. 'endobj' . "\n");
}
/**
* Convert Unicode to CID
*
* @param array{
* 'cidinfo': TFontDataCidInfo,
* 'cw': array<int, int>,
* 'desc': TFontDataDesc,
* 'dw': int,
* 'enc': string,
* 'i': int,
* 'n': int,
* 'name': string,
* 'subset': bool,
* 'subsetchars': array<int, bool>,
* } $font Font to process
* @param int $cidoffset Offset for CID values
*/
protected function uniToCid(array &$font, int $cidoffset): void
{
// convert unicode to cid.
$uni2cid = $font['cidinfo']['uni2cid'];
$chw = [];
foreach ($font['cw'] as $uni => $width) {
if (isset($uni2cid[$uni])) {
$chw[($uni2cid[$uni] + $cidoffset)] = $width;
} elseif ($uni < 256) {
$chw[$uni] = $width;
} // else unknown character
}
$font['cw'] = array_merge($font['cw'], $chw);
}
/**
* Get the PDF output string for a TrueTypeUnicode font.
* Based on PDF Reference 1.3 (section 5)
*
* @param array{
* 'cidinfo': TFontDataCidInfo,
* 'compress': bool,
* 'ctg': string,
* 'cw': array<int, int>,
* 'desc': TFontDataDesc,
* 'dir': string,
* 'dw': int,
* 'enc': string,
* 'file_n': int,
* 'i': int,
* 'n': int,
* 'name': string,
* 'subset': bool,
* 'subsetchars': array<int, bool>,
* } $font Font to process
*
* return string
*
* @SuppressWarnings("PHPMD.ExcessiveMethodLength")
* @SuppressWarnings("PHPMD.CyclomaticComplexity")
* @SuppressWarnings("PHPMD.NPathComplexity")
*/
protected function getTrueTypeUnicode(array $font): string
{
$fontname = '';
if ($font['subset']) {
// change name for font subsetting
$subtag = sprintf('%06u', $font['i']);
$subtag = strtr($subtag, '0123456789', 'ABCDEFGHIJ');
$fontname .= $subtag . '+';
}
$fontname .= $font['name'];
// Type0 Font
// A composite font composed of other fonts, organized hierarchically
// obj 1
$out = $font['n'] . ' 0 obj' . "\n"
. '<< /Type /Font'
. ' /Subtype /Type0'
. ' /BaseFont /' . $fontname
. ' /Name /F' . $font['i']
. ' /Encoding /' . $font['enc']
. ' /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.
$cidinfo = '/Registry ' . $this->enc->escapeDataString($font['cidinfo']['Registry'], $this->pon)
. ' /Ordering ' . $this->enc->escapeDataString($font['cidinfo']['Ordering'], $this->pon)
. ' /Supplement ' . $font['cidinfo']['Supplement'];
$out .= ' /CIDSystemInfo << ' . $cidinfo . ' >>'
. ' /FontDescriptor ' . ($this->pon + 1) . ' 0 R'
. ' /DW ' . $font['dw'] . "\n"
. $this->getCharWidths($font, 0);
if (! empty($font['ctg'])) {
$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 ($font['desc'] as $key => $val) {
$out .= $this->getKeyValOut($key, $val);
}
if (! empty($font['file_n'])) {
// A stream containing a TrueType font
$out .= ' /FontFile2 ' . $font['file_n'] . ' 0 R';
}
$out .= ' >>' . "\n"
. 'endobj' . "\n";
if (! empty($font['ctg'])) {
$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 embedd
$ctgfile = strtolower($font['ctg']);
// search and get ctg font file to embedd
$fontfile = $this->getFontFullPath($font['dir'], $ctgfile);
$content = file_get_contents($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 array{
* 'family': string,
* 'i': int,
* 'n': int,
* 'name': string,
* } $font Font to process
*
* return string
*/
protected function getCore(array $font): string
{
$out = $font['n'] . ' 0 obj' . "\n"
. '<</Type /Font'
. ' /Subtype /Type1'
. ' /BaseFont /' . $font['name']
. ' /Name /F' . $font['i'];
if (($font['family'] != 'symbol') && ($font['family'] != 'zapfdingbats')) {
$out .= ' /Encoding /WinAnsiEncoding';
}
return $out . (' >>' . "\n"
. 'endobj' . "\n");
}
/**
* Get the PDF output string for a Core font.
*
* @param array{
* 'cw': array<int, int>,
* 'desc': TFontDataDesc,
* 'diff_n': int,
* 'dw': int,
* 'enc': string,
* 'file': string,
* 'file_n': int,
* 'i': int,
* 'n': int,
* 'name': string,
* 'type': string,
* } $font Font to process
*
* return string
*/
protected function getTrueType(array $font): string
{
// obj 1
$out = $font['n'] . ' 0 obj' . "\n"
. '<</Type /Font'
. ' /Subtype /' . $font['type']
. ' /BaseFont /' . $font['name']
. ' /Name /F' . $font['i']
. ' /FirstChar 32 /LastChar 255'
. ' /Widths ' . ($this->pon + 1) . ' 0 R'
. ' /FontDescriptor ' . ($this->pon + 2) . ' 0 R';
if (! empty($font['enc'])) {
if (isset($font['diff_n']) && $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($font['cw'][$idx])) {
$out .= $font['cw'][$idx] . ' ';
} else {
$out .= $font['dw'] . ' ';
}
}
$out .= ']' . "\n"
. 'endobj' . "\n";
// obj 3 - Descriptor
$out .= (++$this->pon) . ' 0 obj' . "\n"
. '<</Type /FontDescriptor /FontName /' . $font['name'];
foreach ($font['desc'] as $fdk => $fdv) {
$out .= $this->getKeyValOut($fdk, $fdv);
}
if (! empty($font['file'])) {
$out .= ' /FontFile' . ($font['type'] == 'Type1' ? '' : '2') . ' ' . $font['file_n'] . ' 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 . ' ' . $val . '';
}
}

View File

@@ -0,0 +1,197 @@
<?php
/**
* OutUtil.php
*
* @since 2011-05-23
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2024 Nicola Asuni - Tecnick.com LTD
* @license http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
* @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-2024 Nicola Asuni - Tecnick.com LTD
* @license http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
* @link https://github.com/tecnickcom/tc-lib-pdf-font
*/
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
*/
protected function getFontFullPath(string $fontdir, string $file): string
{
$dirobj = new Dir();
// directories where to search for the font definition file
$dirs = array_unique(
['', $fontdir, (defined('K_PATH_FONTS') ? K_PATH_FONTS : ''), $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 array{
* 'cw': array<int, int>,
* 'dw': int,
* 'subset': bool,
* 'subsetchars': array<int, bool>,
* } $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 array{
* 'cw': array<int, int>,
* 'dw': int,
* 'subset': bool,
* 'subsetchars': array<int, bool>,
* } $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;
}
}
return $this->optimizeWidthRanges($range);
}
/**
* Optimize width ranges
*
* @param array<int, array<int, int>> $range Widht 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;
}
}

View File

@@ -0,0 +1,258 @@
<?php
/**
* Output.php
*
* @since 2011-05-23
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2024 Nicola Asuni - Tecnick.com LTD
* @license http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
* @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\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-2024 Nicola Asuni - Tecnick.com LTD
* @license http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
* @link https://github.com/tecnickcom/tc-lib-pdf-font
*
* @phpstan-import-type TFontData from Load
*/
class Output extends \Com\Tecnick\Pdf\Font\OutFont
{
/**
* 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
*/
public function __construct(
protected array $fonts,
int $pon,
Encrypt $encrypt
) {
$this->pon = $pon;
$this->enc = $encrypt;
$this->out = $this->getEncodingDiffs();
$this->out .= $this->getFontFiles();
$this->out .= $this->getFontDefinitions();
}
/**
* 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, array{'i': int, 'n': int}> $data Font data.
*
* @return string
*/
private function getOutFontResources(array $data): string
{
if (empty($data)) {
return '';
}
$out = ' /Font <<';
foreach ($data as $font) {
$out .= ' /F' . $font['i'] . ' ' . $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 (empty($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 (! empty($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 (! empty($font['file'])) {
$file_key = md5($font['file']);
if (empty($this->subchars[$file_key])) {
$this->subchars[$file_key] = $font['subsetchars'];
} else {
$this->subchars[$file_key] = array_merge($this->subchars[$file_key], $font['subsetchars']);
}
}
}
return $out;
}
/**
* Get the PDF output string for font files
*
* return string
*/
protected function getFontFiles(): string
{
$out = '';
$done = []; // store processed items to avoid duplication
foreach ($this->fonts as $fkey => $font) {
if (! empty($font['file'])) {
$dkey = md5($font['file']);
if (! isset($done[$dkey])) {
$fontfile = $this->getFontFullPath($font['dir'], $font['file']);
$font_data = file_get_contents($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);
}
$sub = new Subset($font_data, $font, $this->subchars[md5($font['file'])]);
$font_data = $sub->getSubsetFont();
$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'];
$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;
}
/**
* 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;
}
}

View File

@@ -0,0 +1,758 @@
<?php
/**
* Stack.php
*
* @since 2011-05-23
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2024 Nicola Asuni - Tecnick.com LTD
* @license http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
* @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\Unicode\Data\Type as UnicodeType;
use Com\Tecnick\Pdf\Font\Exception as FontException;
/**
* Com\Tecnick\Pdf\Font\Stack
*
* @since 2011-05-23
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2024 Nicola Asuni - Tecnick.com LTD
* @license http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
* @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>,
* '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 ?int $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 embedd only a subset of the font
* (stores only the information related to
* the used characters); If false embedd
* full font; This option is valid only for
* TrueTypeUnicode fonts and it 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 = '',
?int $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
$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 instanceof \Com\Tecnick\Pdf\Font\Exception) {
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
*/
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 ?int $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
*/
public function cloneFont(
int &$objnum,
?int $idx = null,
?string $style = null,
?int $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->stack[$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,
$data['ifile'],
$data['subset'],
);
}
/**
* Returns the current font key.
*
* @return string
*/
public function getCurrentFontKey(): string
{
return $this->stack[$this->index]['key'];
}
/**
* Returns the current font type (i.e.: Core, TrueType, TrueTypeUnicode, Type1).
*
* @return string
*/
public function getCurrentFontType(): string
{
return $this->getFont($this->stack[$this->index]['key'])['type'];
}
/**
* Returns the PDF code to use the current font.
*
* @return string
*/
public function getOutCurrentFont(): string
{
return $this->getFontMetric($this->index)['out'];
}
/**
* Returns true if the current font type is Core, TrueType or Type1.
*
* @return bool
*/
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
*/
public function isCurrentUnicodeFont(): bool
{
$currentFontType = $this->getCurrentFontType();
return (($currentFontType == 'TrueTypeUnicode') || ($currentFontType == 'cidfont0'));
}
/**
* Remove and return the last inserted font
*
* @return TFontMetric
*/
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.
*/
public function replaceMissingChars(array $uniarr, array $subs = []): array
{
$font = $this->getFontMetric($this->index);
foreach ($uniarr as $pos => $uni) {
if (isset($font['cw'][$uni])) {
continue;
}
if (! isset($subs[$uni])) {
continue;
}
foreach ($subs[$uni] as $alt) {
if (isset($font['cw'][$alt])) {
$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
*/
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
*/
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);
return $font['cw'][$ord] ?? $font['dw'];
}
/**
* Returns the lenght of the string specified using an array of codepoints.
*
* @param array<int, int> $uniarr Array of character codepoints.
*
* @return float
*/
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
*/
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
$fact = ($this->stack[$this->index]['spacing'] * $this->stack[$this->index]['stretching']);
$uniarr[] = 8203; // add null at the end to ensure that the last word is processed
$split = [];
foreach ($uniarr as $idx => $ord) {
$unitype = UnicodeType::UNI[$ord];
$chrwidth = $this->getCharWidth($ord);
// 'B' Paragraph Separator
// 'S' Segment Separator
// 'WS' Whitespace
// 'BN' Boundary Neutral
if (($unitype == 'B') || ($unitype == 'S') || ($unitype == 'WS') || ($unitype == 'BN')) {
$split[$words] = [
'pos' => $idx,
'ord' => $ord,
'spaces' => $spaces,
'septype' => $unitype,
'wordwidth' => 0,
'totwidth' => ($totwidth + ($fact * ($idx - 1))),
'totspacewidth' => ($totspacewidth + ($fact * ($spaces - 1))),
];
if ($words > 0) {
$split[$words]['wordwidth'] = ($split[$words]['totwidth'] - $split[($words - 1)]['totwidth']);
}
$words++;
if ($unitype == 'WS') {
++$spaces;
$totspacewidth += $chrwidth;
}
}
$totwidth += $chrwidth;
}
$totwidth += ($fact * ($chars - 1));
$totspacewidth += ($fact * ($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)
*/
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 i not defined
*/
public function replaceChar(int $oldchar, int $newchar): int
{
if ($this->isCharDefined($newchar)) {
// add the new char on the subset list
$this->addSubsetChar($this->stack[$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
*/
protected function getFontMetric(int $idx): array
{
$font = $this->stack[$idx];
$mkey = md5(serialize($font));
if (! empty($this->metric[$mkey])) {
return $this->metric[$mkey];
}
$size = $font['size'];
$usize = ((float) $size / $this->kunit);
$cratio = ((float) $size / 1000);
$wratio = ($cratio * $font['stretching']); // horizontal ratio
$data = $this->getFont($font['key']);
$outfont = sprintf('/F%d %F Tf', $data['i'], $font['size']); // PDF output string
// add this font in the stack wit metrics in internal units
$this->metric[$mkey] = [
'ascent' => ((float) $data['desc']['Ascent'] * $cratio),
'avgwidth' => ((float) $data['desc']['AvgWidth'] * $cratio * $font['stretching']),
'capheight' => ((float) $data['desc']['CapHeight'] * $cratio),
'cbbox' => [],
'cratio' => $cratio,
'cw' => [],
'descent' => ((float) $data['desc']['Descent'] * $cratio),
'dw' => ((float) $data['dw'] * $cratio * $font['stretching']),
'fbbox' => [],
'height' => ((float) ($data['desc']['Ascent'] - $data['desc']['Descent']) * $cratio),
'idx' => $idx,
'key' => $font['key'],
'maxwidth' => ((float) $data['desc']['MaxWidth'] * $cratio * $font['stretching']),
'midpoint' => ((float) ($data['desc']['Ascent'] + $data['desc']['Descent']) * $cratio / 2),
'missingwidth' => ((float) $data['desc']['MissingWidth'] * $cratio * $font['stretching']),
'out' => 'BT ' . $outfont . ' ET' . "\r",
'outraw' => $outfont,
'size' => $size,
'spacing' => $font['spacing'],
'stretching' => $font['stretching'],
'style' => $font['style'],
'type' => $data['type'],
'up' => ((float) $data['up'] * $cratio),
'usize' => $usize,
'ut' => ((float) $data['ut'] * $cratio),
'xheight' => ((float) $data['desc']['XHeight'] * $cratio),
];
$tbox = explode(' ', substr($data['desc']['FontBBox'], 1, -1));
$this->metric[$mkey]['fbbox'] = [
// left
((float) $tbox[0] * $wratio),
// bottom
((float) $tbox[1] * $cratio),
// right
((float) $tbox[2] * $wratio),
// top
((float) $tbox[3] * $cratio),
];
//left, bottom, right, and top edges
foreach ($data['cw'] as $cid => $width) {
$this->metric[$mkey]['cw'][(int) $cid] = ((float) $width * $wratio);
}
if (is_array($data['cbbox'])) {
foreach ($data['cbbox'] as $cid => $val) {
if (! is_array($val)) {
continue;
}
if (count($val) != 4) {
continue;
}
$this->metric[$mkey]['cbbox'][(int) $cid] = [
// left
((float) $val[0] * $wratio),
// bottom
((float) $val[1] * $cratio),
// right
((float) $val[2] * $wratio),
// top
((float) $val[3] * $cratio),
];
}
}
return $this->metric[$mkey];
}
/**
* Normalize the input size
*
* @param ?int $size Font size in points (set to null to inherit the last font size).
*
* @return float
*/
protected function getInputSize(?int $size = null): float
{
if (($size === null) || ($size < 0)) {
if ($this->index >= 0) {
// inherit the size of the last inserted font
return $this->stack[$this->index]['size'] ?? 0;
}
return self::DEFAULT_SIZE;
}
return max(0, (float) $size);
}
/**
* Normalize the input spacing
*
* @param ?float $spacing Extra spacing between characters.
*
* @return float
*/
protected function getInputSpacing(?float $spacing = null): float
{
if ($spacing === null) {
if ($this->index >= 0) {
// inherit the size of the last inserted font
return $this->stack[$this->index]['spacing'] ?? 0;
}
return 0;
}
return ($spacing);
}
/**
* Normalize the input stretching
*
* @param ?float $stretching Horizontal character stretching ratio.
*
* @return float
*/
protected function getInputStretching(?float $stretching = null): float
{
if ($stretching === null) {
if ($this->index >= 0) {
// inherit the size of the last inserted font
return $this->stack[$this->index]['stretching'] ?? 0;
}
return 1;
}
return ($stretching);
}
/**
* Return normalized font keys
*
* @param string $fontfamily Property string containing comma-separated font family names
*
* @return array<string>
*/
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) || (! is_string($fontfamily))) {
throw new FontException('Invalid font family name: ' . $fontfamily);
}
// 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 nomalized font family name or the current font name (key.
*
* @param string $fontfamily Raw font family name.
*
* @return string
*/
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();
}
}

View File

@@ -0,0 +1,498 @@
<?php
/**
* Subset.php
*
* @since 2011-05-23
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2024 Nicola Asuni - Tecnick.com LTD
* @license http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
* @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\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-2024 Nicola Asuni - Tecnick.com LTD
* @license http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
* @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' => [],
'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;
/**
* Process TrueType font
*
* @param string $font Content of the input font file
* @param TFontData $fdt Extracted font metrics
* @param array<int, bool> $subchars Array containing subset chars
*
* @throws FontException in case of error
*/
public function __construct(string $font, array $fdt, array $subchars = [])
{
$this->fbyte = new Byte($font);
$trueType = new TrueType($font, $fdt, $this->fbyte, $subchars);
$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;
}
/**
* Returs the checksum of a TTF table.
*
* @param string $table Table to check
* @param int $length Length of table in bytes
*
* @return int checksum
*/
protected function getTableChecksum(string $table, int $length): int
{
$sum = 0;
$tlen = ($length / 4);
$offset = 0;
for ($idx = 0; $idx < $tlen; ++$idx) {
$val = unpack('Ni', substr($table, $offset, 4));
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);
}
$this->subglyphs = [...$this->subglyphs, ...$new_sga];
}
// sort glyphs by key (and remove duplicates)
ksort($this->subglyphs);
}
/**
* Add composite glyphs
*
* @param array<int, bool> $new_sga
*
* @return array<int, bool>
*/
protected function findCompositeGlyphs(array $new_sga, int $key): array
{
if (isset($this->fdt['indexToLoc'][$key])) {
$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
$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
if (($flags & 1) !== 0) {
$this->offset += 4;
} else {
$this->offset += 2;
}
if (($flags & 8) !== 0) {
$this->offset += 2;
} elseif (($flags & 64) !== 0) {
$this->offset += 4;
} elseif (($flags & 128) !== 0) {
$this->offset += 8;
}
} while ($flags & 32);
}
}
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 (empty($this->fdt['table'][$tag])) {
$this->fdt['table'][$tag] = [
'checkSum' => 0,
'data' => '',
'length' => 0,
'offset' => 0,
];
}
$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", $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) {
if (
isset($this->subglyphs[$i])
&& isset($this->fdt['indexToLoc'][$i])
&& isset($this->fdt['indexToLoc'][($i + 1)])
) {
$length = ($this->fdt['indexToLoc'][($i + 1)] - $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 (empty($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 (empty($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']
);
}
/**
* build new subset font
*/
protected function buildSubsetFont(): void
{
$this->subfont = '';
$this->subfont .= pack('N', 0x10000); // 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
$this->offset = ($numTables * 16);
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 = (0xB1B0AFBA - $this->getTableChecksum($this->subfont, strlen($this->subfont)));
$this->subfont = substr($this->subfont, 0, $this->fdt['table']['head']['offset'] + 8)
. pack('N', $checkSumAdjustment)
. substr($this->subfont, $this->fdt['table']['head']['offset'] + 12);
}
}

File diff suppressed because one or more lines are too long