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,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);
}
}